qfai 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,786 @@ 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.0".length > 0) {
893
+ return "0.6.0";
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 scenarioFiles = await collectScenarioFiles(specsRoot);
1025
+ const globs = normalizeGlobs2(config.validation.traceability.testFileGlobs);
1026
+ const exclude = normalizeGlobs2([
1027
+ ...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
1028
+ ...config.validation.traceability.testFileExcludeGlobs
1029
+ ]);
1030
+ try {
1031
+ const matched = globs.length === 0 ? [] : await collectFilesByGlobs(root, { globs, ignore: exclude });
1032
+ const matchedCount = matched.length;
1033
+ const severity = globs.length === 0 ? "warning" : scenarioFiles.length > 0 && config.validation.traceability.scMustHaveTest && matchedCount === 0 ? "warning" : "ok";
1034
+ addCheck(checks, {
1035
+ id: "traceability.testGlobs",
1036
+ severity,
1037
+ title: "Test file globs",
1038
+ message: globs.length === 0 ? "testFileGlobs is empty (SC\u2192Test cannot be verified)" : `matchedFileCount=${matchedCount}`,
1039
+ details: {
1040
+ globs,
1041
+ excludeGlobs: exclude,
1042
+ scenarioFiles: scenarioFiles.length,
1043
+ scMustHaveTest: config.validation.traceability.scMustHaveTest
1044
+ }
1045
+ });
1046
+ } catch (error2) {
1047
+ addCheck(checks, {
1048
+ id: "traceability.testGlobs",
1049
+ severity: "error",
1050
+ title: "Test file globs",
1051
+ message: "Glob scan failed (invalid pattern or filesystem error)",
1052
+ details: { globs, excludeGlobs: exclude, error: String(error2) }
1053
+ });
1054
+ }
1055
+ const outDirAbs = resolvePath(root, config, "outDir");
1056
+ const rel = import_node_path7.default.relative(outDirAbs, validateJsonAbs);
1057
+ const inside = rel !== "" && !rel.startsWith("..") && !import_node_path7.default.isAbsolute(rel);
1058
+ addCheck(checks, {
1059
+ id: "output.pathAlignment",
1060
+ severity: inside ? "ok" : "warning",
1061
+ title: "Output path alignment",
1062
+ message: inside ? "validateJsonPath is under outDir" : "validateJsonPath is not under outDir (may be intended, but check configuration)",
1063
+ details: {
1064
+ outDir: toRelativePath(root, outDirAbs),
1065
+ validateJsonPath: toRelativePath(root, validateJsonAbs)
1066
+ }
1067
+ });
1068
+ return {
1069
+ tool: "qfai",
1070
+ version,
1071
+ doctorFormatVersion: 1,
1072
+ generatedAt,
1073
+ root: toRelativePath(process.cwd(), root),
1074
+ config: {
1075
+ startDir: toRelativePath(process.cwd(), startDir),
1076
+ found: search.found,
1077
+ configPath: toRelativePath(root, search.configPath) || "qfai.config.yaml"
1078
+ },
1079
+ summary: summarize(checks),
1080
+ checks
1081
+ };
1082
+ }
1083
+
1084
+ // src/cli/lib/logger.ts
1085
+ function info(message) {
1086
+ process.stdout.write(`${message}
1087
+ `);
1088
+ }
1089
+ function warn(message) {
1090
+ process.stdout.write(`${message}
1091
+ `);
1092
+ }
1093
+ function error(message) {
1094
+ process.stderr.write(`${message}
1095
+ `);
1096
+ }
1097
+
1098
+ // src/cli/commands/doctor.ts
1099
+ function formatDoctorText(data) {
1100
+ const lines = [];
1101
+ lines.push(
1102
+ `qfai doctor: root=${data.root} config=${data.config.configPath} (${data.config.found ? "found" : "missing"})`
1103
+ );
1104
+ for (const check of data.checks) {
1105
+ lines.push(`[${check.severity}] ${check.id}: ${check.message}`);
1106
+ }
1107
+ lines.push(
1108
+ `summary: ok=${data.summary.ok} warning=${data.summary.warning} error=${data.summary.error}`
1109
+ );
1110
+ return lines.join("\n");
1111
+ }
1112
+ function formatDoctorJson(data) {
1113
+ return JSON.stringify(data, null, 2);
1114
+ }
1115
+ async function runDoctor(options) {
1116
+ const data = await createDoctorData({
1117
+ startDir: options.root,
1118
+ rootExplicit: options.rootExplicit
1119
+ });
1120
+ const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1121
+ if (options.outPath) {
1122
+ const outAbs = import_node_path8.default.isAbsolute(options.outPath) ? options.outPath : import_node_path8.default.resolve(process.cwd(), options.outPath);
1123
+ await (0, import_promises8.mkdir)(import_node_path8.default.dirname(outAbs), { recursive: true });
1124
+ await (0, import_promises8.writeFile)(outAbs, `${output}
1125
+ `, "utf-8");
1126
+ info(`doctor: wrote ${outAbs}`);
1127
+ return;
1128
+ }
1129
+ info(output);
1130
+ }
1131
+
1132
+ // src/cli/commands/init.ts
1133
+ var import_node_path11 = __toESM(require("path"), 1);
1134
+
1135
+ // src/cli/lib/fs.ts
1136
+ var import_promises9 = require("fs/promises");
1137
+ var import_node_path9 = __toESM(require("path"), 1);
1138
+ async function copyTemplateTree(sourceRoot, destRoot, options) {
1139
+ const files = await collectTemplateFiles(sourceRoot);
1140
+ return copyFiles(files, sourceRoot, destRoot, options);
1141
+ }
1142
+ async function copyFiles(files, sourceRoot, destRoot, options) {
1143
+ const copied = [];
1144
+ const skipped = [];
1145
+ const conflicts = [];
1146
+ if (!options.force) {
1147
+ for (const file of files) {
1148
+ const relative = import_node_path9.default.relative(sourceRoot, file);
1149
+ const dest = import_node_path9.default.join(destRoot, relative);
1150
+ if (!await shouldWrite(dest, options.force)) {
1151
+ conflicts.push(dest);
1152
+ }
1153
+ }
1154
+ if (conflicts.length > 0) {
1155
+ throw new Error(formatConflictMessage(conflicts));
1156
+ }
1157
+ }
1158
+ for (const file of files) {
1159
+ const relative = import_node_path9.default.relative(sourceRoot, file);
1160
+ const dest = import_node_path9.default.join(destRoot, relative);
1161
+ if (!await shouldWrite(dest, options.force)) {
1162
+ skipped.push(dest);
1163
+ continue;
1164
+ }
1165
+ if (!options.dryRun) {
1166
+ await (0, import_promises9.mkdir)(import_node_path9.default.dirname(dest), { recursive: true });
1167
+ await (0, import_promises9.copyFile)(file, dest);
1168
+ }
1169
+ copied.push(dest);
1170
+ }
1171
+ return { copied, skipped };
1172
+ }
1173
+ function formatConflictMessage(conflicts) {
1174
+ return [
1175
+ "\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",
1176
+ "",
1177
+ "\u885D\u7A81\u30D5\u30A1\u30A4\u30EB:",
1178
+ ...conflicts.map((conflict) => `- ${conflict}`),
1179
+ "",
1180
+ "\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"
1181
+ ].join("\n");
1182
+ }
1183
+ async function collectTemplateFiles(root) {
1184
+ const entries = [];
1185
+ if (!await exists5(root)) {
1186
+ return entries;
1187
+ }
1188
+ const items = await (0, import_promises9.readdir)(root, { withFileTypes: true });
1189
+ for (const item of items) {
1190
+ const fullPath = import_node_path9.default.join(root, item.name);
1191
+ if (item.isDirectory()) {
1192
+ const nested = await collectTemplateFiles(fullPath);
1193
+ entries.push(...nested);
1194
+ continue;
1195
+ }
1196
+ if (item.isFile()) {
1197
+ entries.push(fullPath);
1198
+ }
1199
+ }
1200
+ return entries;
1201
+ }
1202
+ async function shouldWrite(target, force) {
1203
+ if (force) {
1204
+ return true;
1205
+ }
1206
+ return !await exists5(target);
1207
+ }
1208
+ async function exists5(target) {
1209
+ try {
1210
+ await (0, import_promises9.access)(target);
1211
+ return true;
1212
+ } catch {
1213
+ return false;
1214
+ }
1215
+ }
1216
+
1217
+ // src/cli/lib/assets.ts
1218
+ var import_node_fs = require("fs");
1219
+ var import_node_path10 = __toESM(require("path"), 1);
1220
+ var import_node_url2 = require("url");
1221
+ function getInitAssetsDir() {
1222
+ const base = __filename;
1223
+ const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
1224
+ const baseDir = import_node_path10.default.dirname(basePath);
1225
+ const candidates = [
1226
+ import_node_path10.default.resolve(baseDir, "../../../assets/init"),
1227
+ import_node_path10.default.resolve(baseDir, "../../assets/init")
1228
+ ];
1229
+ for (const candidate of candidates) {
1230
+ if ((0, import_node_fs.existsSync)(candidate)) {
1231
+ return candidate;
1232
+ }
1233
+ }
1234
+ throw new Error(
1235
+ [
1236
+ "init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
1237
+ "\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
1238
+ ...candidates.map((candidate) => `- ${candidate}`)
1239
+ ].join("\n")
1240
+ );
1241
+ }
1242
+
1243
+ // src/cli/commands/init.ts
1244
+ async function runInit(options) {
1245
+ const assetsRoot = getInitAssetsDir();
1246
+ const rootAssets = import_node_path11.default.join(assetsRoot, "root");
1247
+ const qfaiAssets = import_node_path11.default.join(assetsRoot, ".qfai");
1248
+ const destRoot = import_node_path11.default.resolve(options.dir);
1249
+ const destQfai = import_node_path11.default.join(destRoot, ".qfai");
1250
+ const rootResult = await copyTemplateTree(rootAssets, destRoot, {
1251
+ force: options.force,
1252
+ dryRun: options.dryRun
1253
+ });
1254
+ const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
1255
+ force: options.force,
1256
+ dryRun: options.dryRun
1257
+ });
1258
+ report(
1259
+ [...rootResult.copied, ...qfaiResult.copied],
1260
+ [...rootResult.skipped, ...qfaiResult.skipped],
1261
+ options.dryRun,
1262
+ "init"
1263
+ );
1264
+ }
1265
+ function report(copied, skipped, dryRun, label) {
1266
+ info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
1267
+ if (copied.length > 0) {
1268
+ info(` created: ${copied.length}`);
1269
+ }
1270
+ if (skipped.length > 0) {
1271
+ info(` skipped: ${skipped.length}`);
1272
+ }
1273
+ }
1274
+
1275
+ // src/cli/commands/report.ts
1276
+ var import_promises18 = require("fs/promises");
1277
+ var import_node_path18 = __toESM(require("path"), 1);
1278
+
1279
+ // src/core/normalize.ts
1280
+ function normalizeIssuePaths(root, issues) {
1281
+ return issues.map((issue7) => {
1282
+ if (!issue7.file) {
1283
+ return issue7;
1284
+ }
1285
+ const normalized = toRelativePath(root, issue7.file);
1286
+ if (normalized === issue7.file) {
1287
+ return issue7;
1288
+ }
1289
+ return {
1290
+ ...issue7,
1291
+ file: normalized
1292
+ };
1293
+ });
1294
+ }
1295
+ function normalizeScCoverage(root, sc) {
1296
+ const refs = {};
1297
+ for (const [scId, files] of Object.entries(sc.refs)) {
1298
+ refs[scId] = files.map((file) => toRelativePath(root, file));
1299
+ }
1300
+ return {
1301
+ ...sc,
1302
+ refs
1303
+ };
1304
+ }
1305
+ function normalizeValidationResult(root, result) {
1306
+ return {
1307
+ ...result,
1308
+ issues: normalizeIssuePaths(root, result.issues),
1309
+ traceability: {
1310
+ ...result.traceability,
1311
+ sc: normalizeScCoverage(root, result.traceability.sc)
1312
+ }
1313
+ };
1314
+ }
1315
+
1316
+ // src/core/report.ts
1317
+ var import_promises17 = require("fs/promises");
1318
+ var import_node_path17 = __toESM(require("path"), 1);
1319
+
1320
+ // src/core/contractIndex.ts
1321
+ var import_promises10 = require("fs/promises");
1322
+ var import_node_path12 = __toESM(require("path"), 1);
1323
+
1324
+ // src/core/contractsDecl.ts
1325
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
1326
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
1327
+ function extractDeclaredContractIds(text) {
1328
+ const ids = [];
1329
+ for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
1330
+ const id = match[1];
1331
+ if (id) {
1332
+ ids.push(id);
1333
+ }
1334
+ }
1335
+ return ids;
1336
+ }
1337
+ function stripContractDeclarationLines(text) {
1338
+ return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
1339
+ }
1340
+
1341
+ // src/core/contractIndex.ts
1342
+ async function buildContractIndex(root, config) {
1343
+ const contractsRoot = resolvePath(root, config, "contractsDir");
1344
+ const uiRoot = import_node_path12.default.join(contractsRoot, "ui");
1345
+ const apiRoot = import_node_path12.default.join(contractsRoot, "api");
1346
+ const dbRoot = import_node_path12.default.join(contractsRoot, "db");
1347
+ const [uiFiles, apiFiles, dbFiles] = await Promise.all([
1348
+ collectUiContractFiles(uiRoot),
1349
+ collectApiContractFiles(apiRoot),
1350
+ collectDbContractFiles(dbRoot)
1351
+ ]);
1352
+ const index = {
1353
+ ids: /* @__PURE__ */ new Set(),
1354
+ idToFiles: /* @__PURE__ */ new Map(),
1355
+ files: { ui: uiFiles, api: apiFiles, db: dbFiles }
1356
+ };
1357
+ await indexContractFiles(uiFiles, index);
1358
+ await indexContractFiles(apiFiles, index);
1359
+ await indexContractFiles(dbFiles, index);
1360
+ return index;
1361
+ }
1362
+ async function indexContractFiles(files, index) {
1363
+ for (const file of files) {
1364
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1365
+ extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
1366
+ }
1367
+ }
1368
+ function record(index, id, file) {
1369
+ index.ids.add(id);
1370
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
1371
+ current.add(file);
1372
+ index.idToFiles.set(id, current);
1373
+ }
1374
+
1375
+ // src/core/ids.ts
1376
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
1377
+ var STRICT_ID_PATTERNS = {
1378
+ SPEC: /\bSPEC-\d{4}\b/g,
1379
+ BR: /\bBR-\d{4}\b/g,
1380
+ SC: /\bSC-\d{4}\b/g,
1381
+ UI: /\bUI-\d{4}\b/g,
1382
+ API: /\bAPI-\d{4}\b/g,
1383
+ DB: /\bDB-\d{4}\b/g,
1384
+ ADR: /\bADR-\d{4}\b/g
1385
+ };
1386
+ var LOOSE_ID_PATTERNS = {
1387
+ SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
1388
+ BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
1389
+ SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
1390
+ UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
1391
+ API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
1392
+ DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
1393
+ ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
1394
+ };
1395
+ function extractIds(text, prefix) {
1396
+ const pattern = STRICT_ID_PATTERNS[prefix];
1397
+ const matches = text.match(pattern);
1398
+ return unique2(matches ?? []);
1399
+ }
1400
+ function extractAllIds(text) {
1401
+ const all = [];
1402
+ ID_PREFIXES.forEach((prefix) => {
1403
+ all.push(...extractIds(text, prefix));
1404
+ });
1405
+ return unique2(all);
1406
+ }
1407
+ function extractInvalidIds(text, prefixes) {
1408
+ const invalid = [];
1409
+ for (const prefix of prefixes) {
1410
+ const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
1411
+ for (const candidate of candidates) {
1412
+ if (!isValidId(candidate, prefix)) {
1413
+ invalid.push(candidate);
1414
+ }
1415
+ }
1416
+ }
1417
+ return unique2(invalid);
1418
+ }
1419
+ function unique2(values) {
1420
+ return Array.from(new Set(values));
1421
+ }
1422
+ function isValidId(value, prefix) {
1423
+ const pattern = STRICT_ID_PATTERNS[prefix];
1424
+ const strict = new RegExp(pattern.source);
1425
+ return strict.test(value);
1426
+ }
1427
+
1428
+ // src/core/parse/contractRefs.ts
1429
+ var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
1430
+ function parseContractRefs(text, options = {}) {
1431
+ const linePattern = buildLinePattern(options);
1432
+ const lines = [];
1433
+ for (const match of text.matchAll(linePattern)) {
1434
+ lines.push((match[1] ?? "").trim());
1435
+ }
1436
+ const ids = [];
1437
+ const invalidTokens = [];
1438
+ let hasNone = false;
1439
+ for (const line of lines) {
1440
+ if (line.length === 0) {
1441
+ invalidTokens.push("(empty)");
1306
1442
  continue;
1307
1443
  }
1308
- for (const scId of scIds) {
1309
- const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
1310
- current.add(file);
1311
- refs.set(scId, current);
1444
+ const tokens = line.split(",").map((token) => token.trim());
1445
+ for (const token of tokens) {
1446
+ if (token.length === 0) {
1447
+ invalidTokens.push("(empty)");
1448
+ continue;
1449
+ }
1450
+ if (token === "none") {
1451
+ hasNone = true;
1452
+ continue;
1453
+ }
1454
+ if (CONTRACT_REF_ID_RE.test(token)) {
1455
+ ids.push(token);
1456
+ continue;
1457
+ }
1458
+ invalidTokens.push(token);
1312
1459
  }
1313
1460
  }
1314
1461
  return {
1315
- refs,
1316
- scan: {
1317
- globs: normalizedGlobs,
1318
- excludeGlobs: mergedExcludeGlobs,
1319
- matchedFileCount: normalizedFiles.length
1320
- }
1462
+ lines,
1463
+ ids: unique3(ids),
1464
+ invalidTokens: unique3(invalidTokens),
1465
+ hasNone
1321
1466
  };
1322
1467
  }
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
- };
1468
+ function buildLinePattern(options) {
1469
+ const prefix = options.allowCommentPrefix ? "#" : "";
1470
+ return new RegExp(
1471
+ `^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
1472
+ "gm"
1473
+ );
1345
1474
  }
1346
- function toSortedArray(values) {
1347
- return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
1475
+ function unique3(values) {
1476
+ return Array.from(new Set(values));
1348
1477
  }
1349
- function normalizeGlobs(globs) {
1350
- return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1478
+
1479
+ // src/core/parse/markdown.ts
1480
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1481
+ function parseHeadings(md) {
1482
+ const lines = md.split(/\r?\n/);
1483
+ const headings = [];
1484
+ for (let i = 0; i < lines.length; i++) {
1485
+ const line = lines[i] ?? "";
1486
+ const match = line.match(HEADING_RE);
1487
+ if (!match) continue;
1488
+ const levelToken = match[1];
1489
+ const title = match[2];
1490
+ if (!levelToken || !title) continue;
1491
+ headings.push({
1492
+ level: levelToken.length,
1493
+ title: title.trim(),
1494
+ line: i + 1
1495
+ });
1496
+ }
1497
+ return headings;
1351
1498
  }
1352
- function formatError3(error2) {
1353
- if (error2 instanceof Error) {
1354
- return error2.message;
1499
+ function extractH2Sections(md) {
1500
+ const lines = md.split(/\r?\n/);
1501
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1502
+ const sections = /* @__PURE__ */ new Map();
1503
+ for (let i = 0; i < headings.length; i++) {
1504
+ const current = headings[i];
1505
+ if (!current) continue;
1506
+ const next = headings[i + 1];
1507
+ const startLine = current.line + 1;
1508
+ const endLine = (next?.line ?? lines.length + 1) - 1;
1509
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1510
+ sections.set(current.title.trim(), {
1511
+ title: current.title.trim(),
1512
+ startLine,
1513
+ endLine,
1514
+ body
1515
+ });
1355
1516
  }
1356
- return String(error2);
1517
+ return sections;
1357
1518
  }
1358
1519
 
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";
1520
+ // src/core/parse/spec.ts
1521
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1522
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1523
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1524
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1525
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1526
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1527
+ function parseSpec(md, file) {
1528
+ const headings = parseHeadings(md);
1529
+ const h1 = headings.find((heading) => heading.level === 1);
1530
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1531
+ const sections = extractH2Sections(md);
1532
+ const sectionNames = new Set(Array.from(sections.keys()));
1533
+ const brSection = sections.get(BR_SECTION_TITLE);
1534
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1535
+ const startLine = brSection?.startLine ?? 1;
1536
+ const brs = [];
1537
+ const brsWithoutPriority = [];
1538
+ const brsWithInvalidPriority = [];
1539
+ for (let i = 0; i < brLines.length; i++) {
1540
+ const lineText = brLines[i] ?? "";
1541
+ const lineNumber = startLine + i;
1542
+ const validMatch = lineText.match(BR_LINE_RE);
1543
+ if (validMatch) {
1544
+ const id = validMatch[1];
1545
+ const priority = validMatch[2];
1546
+ const text = validMatch[3];
1547
+ if (!id || !priority || !text) continue;
1548
+ brs.push({
1549
+ id,
1550
+ priority,
1551
+ text: text.trim(),
1552
+ line: lineNumber
1553
+ });
1554
+ continue;
1555
+ }
1556
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1557
+ if (anyPriorityMatch) {
1558
+ const id = anyPriorityMatch[1];
1559
+ const priority = anyPriorityMatch[2];
1560
+ const text = anyPriorityMatch[3];
1561
+ if (!id || !priority || !text) continue;
1562
+ if (!VALID_PRIORITIES.has(priority)) {
1563
+ brsWithInvalidPriority.push({
1564
+ id,
1565
+ priority,
1566
+ text: text.trim(),
1567
+ line: lineNumber
1568
+ });
1569
+ }
1570
+ continue;
1571
+ }
1572
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1573
+ if (noPriorityMatch) {
1574
+ const id = noPriorityMatch[1];
1575
+ const text = noPriorityMatch[2];
1576
+ if (!id || !text) continue;
1577
+ brsWithoutPriority.push({
1578
+ id,
1579
+ text: text.trim(),
1580
+ line: lineNumber
1581
+ });
1582
+ }
1366
1583
  }
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";
1584
+ const parsed = {
1585
+ file,
1586
+ sections: sectionNames,
1587
+ brs,
1588
+ brsWithoutPriority,
1589
+ brsWithInvalidPriority,
1590
+ contractRefs: parseContractRefs(md)
1591
+ };
1592
+ if (specId) {
1593
+ parsed.specId = specId;
1375
1594
  }
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");
1595
+ return parsed;
1381
1596
  }
1382
1597
 
1383
1598
  // src/core/validators/contracts.ts
1384
- var import_promises9 = require("fs/promises");
1385
- var import_node_path12 = __toESM(require("path"), 1);
1599
+ var import_promises11 = require("fs/promises");
1600
+ var import_node_path14 = __toESM(require("path"), 1);
1386
1601
 
1387
1602
  // src/core/contracts.ts
1388
- var import_node_path11 = __toESM(require("path"), 1);
1603
+ var import_node_path13 = __toESM(require("path"), 1);
1389
1604
  var import_yaml2 = require("yaml");
1390
1605
  function parseStructuredContract(file, text) {
1391
- const ext = import_node_path11.default.extname(file).toLowerCase();
1606
+ const ext = import_node_path13.default.extname(file).toLowerCase();
1392
1607
  if (ext === ".json") {
1393
1608
  return JSON.parse(text);
1394
1609
  }
@@ -1408,9 +1623,9 @@ var SQL_DANGEROUS_PATTERNS = [
1408
1623
  async function validateContracts(root, config) {
1409
1624
  const issues = [];
1410
1625
  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")));
1626
+ issues.push(...await validateUiContracts(import_node_path14.default.join(contractsRoot, "ui")));
1627
+ issues.push(...await validateApiContracts(import_node_path14.default.join(contractsRoot, "api")));
1628
+ issues.push(...await validateDbContracts(import_node_path14.default.join(contractsRoot, "db")));
1414
1629
  const contractIndex = await buildContractIndex(root, config);
1415
1630
  issues.push(...validateDuplicateContractIds(contractIndex));
1416
1631
  return issues;
@@ -1430,7 +1645,7 @@ async function validateUiContracts(uiRoot) {
1430
1645
  }
1431
1646
  const issues = [];
1432
1647
  for (const file of files) {
1433
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1648
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1434
1649
  const invalidIds = extractInvalidIds(text, [
1435
1650
  "SPEC",
1436
1651
  "BR",
@@ -1485,7 +1700,7 @@ async function validateApiContracts(apiRoot) {
1485
1700
  }
1486
1701
  const issues = [];
1487
1702
  for (const file of files) {
1488
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1703
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1489
1704
  const invalidIds = extractInvalidIds(text, [
1490
1705
  "SPEC",
1491
1706
  "BR",
@@ -1553,7 +1768,7 @@ async function validateDbContracts(dbRoot) {
1553
1768
  }
1554
1769
  const issues = [];
1555
1770
  for (const file of files) {
1556
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1771
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1557
1772
  const invalidIds = extractInvalidIds(text, [
1558
1773
  "SPEC",
1559
1774
  "BR",
@@ -1692,8 +1907,8 @@ function issue(code, message, severity, file, rule, refs) {
1692
1907
  }
1693
1908
 
1694
1909
  // src/core/validators/delta.ts
1695
- var import_promises10 = require("fs/promises");
1696
- var import_node_path13 = __toESM(require("path"), 1);
1910
+ var import_promises12 = require("fs/promises");
1911
+ var import_node_path15 = __toESM(require("path"), 1);
1697
1912
  var SECTION_RE = /^##\s+変更区分/m;
1698
1913
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1699
1914
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1707,10 +1922,10 @@ async function validateDeltas(root, config) {
1707
1922
  }
1708
1923
  const issues = [];
1709
1924
  for (const pack of packs) {
1710
- const deltaPath = import_node_path13.default.join(pack, "delta.md");
1925
+ const deltaPath = import_node_path15.default.join(pack, "delta.md");
1711
1926
  let text;
1712
1927
  try {
1713
- text = await (0, import_promises10.readFile)(deltaPath, "utf-8");
1928
+ text = await (0, import_promises12.readFile)(deltaPath, "utf-8");
1714
1929
  } catch (error2) {
1715
1930
  if (isMissingFileError2(error2)) {
1716
1931
  issues.push(
@@ -1782,8 +1997,8 @@ function issue2(code, message, severity, file, rule, refs) {
1782
1997
  }
1783
1998
 
1784
1999
  // src/core/validators/ids.ts
1785
- var import_promises11 = require("fs/promises");
1786
- var import_node_path14 = __toESM(require("path"), 1);
2000
+ var import_promises13 = require("fs/promises");
2001
+ var import_node_path16 = __toESM(require("path"), 1);
1787
2002
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1788
2003
  async function validateDefinedIds(root, config) {
1789
2004
  const issues = [];
@@ -1818,7 +2033,7 @@ async function validateDefinedIds(root, config) {
1818
2033
  }
1819
2034
  async function collectSpecDefinitionIds(files, out) {
1820
2035
  for (const file of files) {
1821
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2036
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
1822
2037
  const parsed = parseSpec(text, file);
1823
2038
  if (parsed.specId) {
1824
2039
  recordId(out, parsed.specId, file);
@@ -1828,7 +2043,7 @@ async function collectSpecDefinitionIds(files, out) {
1828
2043
  }
1829
2044
  async function collectScenarioDefinitionIds(files, out) {
1830
2045
  for (const file of files) {
1831
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2046
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
1832
2047
  const { document, errors } = parseScenarioDocument(text, file);
1833
2048
  if (!document || errors.length > 0) {
1834
2049
  continue;
@@ -1849,7 +2064,7 @@ function recordId(out, id, file) {
1849
2064
  }
1850
2065
  function formatFileList(files, root) {
1851
2066
  return files.map((file) => {
1852
- const relative = import_node_path14.default.relative(root, file);
2067
+ const relative = import_node_path16.default.relative(root, file);
1853
2068
  return relative.length > 0 ? relative : file;
1854
2069
  }).join(", ");
1855
2070
  }
@@ -1872,7 +2087,7 @@ function issue3(code, message, severity, file, rule, refs) {
1872
2087
  }
1873
2088
 
1874
2089
  // src/core/validators/scenario.ts
1875
- var import_promises12 = require("fs/promises");
2090
+ var import_promises14 = require("fs/promises");
1876
2091
  var GIVEN_PATTERN = /\bGiven\b/;
1877
2092
  var WHEN_PATTERN = /\bWhen\b/;
1878
2093
  var THEN_PATTERN = /\bThen\b/;
@@ -1898,7 +2113,7 @@ async function validateScenarios(root, config) {
1898
2113
  for (const entry of entries) {
1899
2114
  let text;
1900
2115
  try {
1901
- text = await (0, import_promises12.readFile)(entry.scenarioPath, "utf-8");
2116
+ text = await (0, import_promises14.readFile)(entry.scenarioPath, "utf-8");
1902
2117
  } catch (error2) {
1903
2118
  if (isMissingFileError3(error2)) {
1904
2119
  issues.push(
@@ -2068,7 +2283,7 @@ function isMissingFileError3(error2) {
2068
2283
  }
2069
2284
 
2070
2285
  // src/core/validators/spec.ts
2071
- var import_promises13 = require("fs/promises");
2286
+ var import_promises15 = require("fs/promises");
2072
2287
  async function validateSpecs(root, config) {
2073
2288
  const specsRoot = resolvePath(root, config, "specsDir");
2074
2289
  const entries = await collectSpecEntries(specsRoot);
@@ -2089,7 +2304,7 @@ async function validateSpecs(root, config) {
2089
2304
  for (const entry of entries) {
2090
2305
  let text;
2091
2306
  try {
2092
- text = await (0, import_promises13.readFile)(entry.specPath, "utf-8");
2307
+ text = await (0, import_promises15.readFile)(entry.specPath, "utf-8");
2093
2308
  } catch (error2) {
2094
2309
  if (isMissingFileError4(error2)) {
2095
2310
  issues.push(
@@ -2238,7 +2453,7 @@ function isMissingFileError4(error2) {
2238
2453
  }
2239
2454
 
2240
2455
  // src/core/validators/traceability.ts
2241
- var import_promises14 = require("fs/promises");
2456
+ var import_promises16 = require("fs/promises");
2242
2457
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
2243
2458
  var BR_TAG_RE2 = /^BR-\d{4}$/;
2244
2459
  async function validateTraceability(root, config) {
@@ -2258,7 +2473,7 @@ async function validateTraceability(root, config) {
2258
2473
  const contractIndex = await buildContractIndex(root, config);
2259
2474
  const contractIds = contractIndex.ids;
2260
2475
  for (const file of specFiles) {
2261
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2476
+ const text = await (0, import_promises16.readFile)(file, "utf-8");
2262
2477
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2263
2478
  const parsed = parseSpec(text, file);
2264
2479
  if (parsed.specId) {
@@ -2331,7 +2546,7 @@ async function validateTraceability(root, config) {
2331
2546
  }
2332
2547
  }
2333
2548
  for (const file of scenarioFiles) {
2334
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2549
+ const text = await (0, import_promises16.readFile)(file, "utf-8");
2335
2550
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2336
2551
  const scenarioContractRefs = parseContractRefs(text, {
2337
2552
  allowCommentPrefix: true
@@ -2653,7 +2868,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2653
2868
  const pattern = buildIdPattern(Array.from(upstreamIds));
2654
2869
  let found = false;
2655
2870
  for (const file of targetFiles) {
2656
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2871
+ const text = await (0, import_promises16.readFile)(file, "utf-8");
2657
2872
  if (pattern.test(text)) {
2658
2873
  found = true;
2659
2874
  break;
@@ -2740,15 +2955,15 @@ function countIssues(issues) {
2740
2955
  // src/core/report.ts
2741
2956
  var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2742
2957
  async function createReportData(root, validation, configResult) {
2743
- const resolvedRoot = import_node_path15.default.resolve(root);
2958
+ const resolvedRoot = import_node_path17.default.resolve(root);
2744
2959
  const resolved = configResult ?? await loadConfig(resolvedRoot);
2745
2960
  const config = resolved.config;
2746
2961
  const configPath = resolved.configPath;
2747
2962
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
2748
2963
  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");
2964
+ const apiRoot = import_node_path17.default.join(contractsRoot, "api");
2965
+ const uiRoot = import_node_path17.default.join(contractsRoot, "ui");
2966
+ const dbRoot = import_node_path17.default.join(contractsRoot, "db");
2752
2967
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
2753
2968
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
2754
2969
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2806,11 +3021,13 @@ async function createReportData(root, validation, configResult) {
2806
3021
  normalizeScSources(resolvedRoot, scSources)
2807
3022
  );
2808
3023
  const version = await resolveToolVersion();
3024
+ const reportFormatVersion = 1;
2809
3025
  const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
2810
3026
  const displayConfigPath = toRelativePath(resolvedRoot, configPath);
2811
3027
  return {
2812
3028
  tool: "qfai",
2813
3029
  version,
3030
+ reportFormatVersion,
2814
3031
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2815
3032
  root: displayRoot,
2816
3033
  configPath: displayConfigPath,
@@ -3063,7 +3280,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
3063
3280
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
3064
3281
  }
3065
3282
  for (const file of specFiles) {
3066
- const text = await (0, import_promises15.readFile)(file, "utf-8");
3283
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
3067
3284
  const parsed = parseSpec(text, file);
3068
3285
  const specKey = parsed.specId;
3069
3286
  if (!specKey) {
@@ -3104,7 +3321,7 @@ async function collectIds(files) {
3104
3321
  DB: /* @__PURE__ */ new Set()
3105
3322
  };
3106
3323
  for (const file of files) {
3107
- const text = await (0, import_promises15.readFile)(file, "utf-8");
3324
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
3108
3325
  for (const prefix of ID_PREFIXES2) {
3109
3326
  const ids = extractIds(text, prefix);
3110
3327
  ids.forEach((id) => result[prefix].add(id));
@@ -3122,7 +3339,7 @@ async function collectIds(files) {
3122
3339
  async function collectUpstreamIds(files) {
3123
3340
  const ids = /* @__PURE__ */ new Set();
3124
3341
  for (const file of files) {
3125
- const text = await (0, import_promises15.readFile)(file, "utf-8");
3342
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
3126
3343
  extractAllIds(text).forEach((id) => ids.add(id));
3127
3344
  }
3128
3345
  return ids;
@@ -3143,7 +3360,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
3143
3360
  }
3144
3361
  const pattern = buildIdPattern2(Array.from(upstreamIds));
3145
3362
  for (const file of targetFiles) {
3146
- const text = await (0, import_promises15.readFile)(file, "utf-8");
3363
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
3147
3364
  if (pattern.test(text)) {
3148
3365
  return true;
3149
3366
  }
@@ -3235,7 +3452,7 @@ function buildHotspots(issues) {
3235
3452
 
3236
3453
  // src/cli/commands/report.ts
3237
3454
  async function runReport(options) {
3238
- const root = import_node_path16.default.resolve(options.root);
3455
+ const root = import_node_path18.default.resolve(options.root);
3239
3456
  const configResult = await loadConfig(root);
3240
3457
  let validation;
3241
3458
  if (options.runValidate) {
@@ -3252,7 +3469,7 @@ async function runReport(options) {
3252
3469
  validation = normalized;
3253
3470
  } else {
3254
3471
  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);
3472
+ const inputPath = import_node_path18.default.isAbsolute(input) ? input : import_node_path18.default.resolve(root, input);
3256
3473
  try {
3257
3474
  validation = await readValidationResult(inputPath);
3258
3475
  } catch (err) {
@@ -3278,11 +3495,11 @@ async function runReport(options) {
3278
3495
  const data = await createReportData(root, validation, configResult);
3279
3496
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
3280
3497
  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");
3498
+ const defaultOut = options.format === "json" ? import_node_path18.default.join(outRoot, "report.json") : import_node_path18.default.join(outRoot, "report.md");
3282
3499
  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}
3500
+ const outPath = import_node_path18.default.isAbsolute(out) ? out : import_node_path18.default.resolve(root, out);
3501
+ await (0, import_promises18.mkdir)(import_node_path18.default.dirname(outPath), { recursive: true });
3502
+ await (0, import_promises18.writeFile)(outPath, `${output}
3286
3503
  `, "utf-8");
3287
3504
  info(
3288
3505
  `report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
@@ -3290,7 +3507,7 @@ async function runReport(options) {
3290
3507
  info(`wrote report: ${outPath}`);
3291
3508
  }
3292
3509
  async function readValidationResult(inputPath) {
3293
- const raw = await (0, import_promises16.readFile)(inputPath, "utf-8");
3510
+ const raw = await (0, import_promises18.readFile)(inputPath, "utf-8");
3294
3511
  const parsed = JSON.parse(raw);
3295
3512
  if (!isValidationResult(parsed)) {
3296
3513
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -3346,15 +3563,15 @@ function isMissingFileError5(error2) {
3346
3563
  return record2.code === "ENOENT";
3347
3564
  }
3348
3565
  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)}
3566
+ const abs = import_node_path18.default.isAbsolute(outputPath) ? outputPath : import_node_path18.default.resolve(root, outputPath);
3567
+ await (0, import_promises18.mkdir)(import_node_path18.default.dirname(abs), { recursive: true });
3568
+ await (0, import_promises18.writeFile)(abs, `${JSON.stringify(result, null, 2)}
3352
3569
  `, "utf-8");
3353
3570
  }
3354
3571
 
3355
3572
  // src/cli/commands/validate.ts
3356
- var import_promises17 = require("fs/promises");
3357
- var import_node_path17 = __toESM(require("path"), 1);
3573
+ var import_promises19 = require("fs/promises");
3574
+ var import_node_path19 = __toESM(require("path"), 1);
3358
3575
 
3359
3576
  // src/cli/lib/failOn.ts
3360
3577
  function shouldFail(result, failOn) {
@@ -3369,7 +3586,7 @@ function shouldFail(result, failOn) {
3369
3586
 
3370
3587
  // src/cli/commands/validate.ts
3371
3588
  async function runValidate(options) {
3372
- const root = import_node_path17.default.resolve(options.root);
3589
+ const root = import_node_path19.default.resolve(options.root);
3373
3590
  const configResult = await loadConfig(root);
3374
3591
  const result = await validateProject(root, configResult);
3375
3592
  const normalized = normalizeValidationResult(root, result);
@@ -3486,12 +3703,12 @@ function issueKey(issue7) {
3486
3703
  }
3487
3704
  async function emitJson(result, root, jsonPath) {
3488
3705
  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)}
3706
+ await (0, import_promises19.mkdir)(import_node_path19.default.dirname(abs), { recursive: true });
3707
+ await (0, import_promises19.writeFile)(abs, `${JSON.stringify(result, null, 2)}
3491
3708
  `, "utf-8");
3492
3709
  }
3493
3710
  function resolveJsonPath(root, jsonPath) {
3494
- return import_node_path17.default.isAbsolute(jsonPath) ? jsonPath : import_node_path17.default.resolve(root, jsonPath);
3711
+ return import_node_path19.default.isAbsolute(jsonPath) ? jsonPath : import_node_path19.default.resolve(root, jsonPath);
3495
3712
  }
3496
3713
  var GITHUB_ANNOTATION_LIMIT = 100;
3497
3714
 
@@ -3506,6 +3723,7 @@ function parseArgs(argv, cwd) {
3506
3723
  dryRun: false,
3507
3724
  reportFormat: "md",
3508
3725
  reportRunValidate: false,
3726
+ doctorFormat: "text",
3509
3727
  validateFormat: "text",
3510
3728
  strict: false,
3511
3729
  help: false
@@ -3558,7 +3776,11 @@ function parseArgs(argv, cwd) {
3558
3776
  {
3559
3777
  const next = args[i + 1];
3560
3778
  if (next) {
3561
- options.reportOut = next;
3779
+ if (command === "doctor") {
3780
+ options.doctorOut = next;
3781
+ } else {
3782
+ options.reportOut = next;
3783
+ }
3562
3784
  }
3563
3785
  }
3564
3786
  i += 1;
@@ -3601,6 +3823,12 @@ function applyFormatOption(command, value, options) {
3601
3823
  }
3602
3824
  return;
3603
3825
  }
3826
+ if (command === "doctor") {
3827
+ if (value === "text" || value === "json") {
3828
+ options.doctorFormat = value;
3829
+ }
3830
+ return;
3831
+ }
3604
3832
  if (value === "md" || value === "json") {
3605
3833
  options.reportFormat = value;
3606
3834
  }
@@ -3648,6 +3876,14 @@ async function run(argv, cwd) {
3648
3876
  });
3649
3877
  }
3650
3878
  return;
3879
+ case "doctor":
3880
+ await runDoctor({
3881
+ root: options.root,
3882
+ rootExplicit: options.rootExplicit,
3883
+ format: options.doctorFormat,
3884
+ ...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {}
3885
+ });
3886
+ return;
3651
3887
  default:
3652
3888
  error(`Unknown command: ${command}`);
3653
3889
  info(usage());
@@ -3661,6 +3897,7 @@ Commands:
3661
3897
  init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
3662
3898
  validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
3663
3899
  report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
3900
+ doctor \u8A2D\u5B9A/\u30D1\u30B9/\u51FA\u529B\u524D\u63D0\u306E\u8A3A\u65AD
3664
3901
 
3665
3902
  Options:
3666
3903
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
@@ -3670,9 +3907,10 @@ Options:
3670
3907
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
3671
3908
  --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
3672
3909
  --format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
3910
+ --format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
3673
3911
  --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
3674
3912
  --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
3675
- --out <path> report: \u51FA\u529B\u5148
3913
+ --out <path> report/doctor: \u51FA\u529B\u5148
3676
3914
  --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
3677
3915
  --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
3678
3916
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A