qfai 0.5.0 → 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,166 +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 error(message) {
143
- process.stderr.write(`${message}
144
- `);
145
- }
146
-
147
- // src/cli/commands/init.ts
148
- async function runInit(options) {
149
- const assetsRoot = getInitAssetsDir();
150
- const rootAssets = import_node_path3.default.join(assetsRoot, "root");
151
- const qfaiAssets = import_node_path3.default.join(assetsRoot, ".qfai");
152
- const destRoot = import_node_path3.default.resolve(options.dir);
153
- const destQfai = import_node_path3.default.join(destRoot, ".qfai");
154
- const rootResult = await copyTemplateTree(rootAssets, destRoot, {
155
- force: options.force,
156
- dryRun: options.dryRun
157
- });
158
- const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
159
- force: options.force,
160
- dryRun: options.dryRun
161
- });
162
- report(
163
- [...rootResult.copied, ...qfaiResult.copied],
164
- [...rootResult.skipped, ...qfaiResult.skipped],
165
- options.dryRun,
166
- "init"
167
- );
168
- }
169
- function report(copied, skipped, dryRun, label) {
170
- info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
171
- if (copied.length > 0) {
172
- info(` created: ${copied.length}`);
173
- }
174
- if (skipped.length > 0) {
175
- info(` skipped: ${skipped.length}`);
176
- }
177
- }
26
+ // src/cli/commands/doctor.ts
27
+ var import_promises8 = require("fs/promises");
28
+ var import_node_path8 = __toESM(require("path"), 1);
178
29
 
179
- // src/cli/commands/report.ts
180
- var import_promises16 = require("fs/promises");
181
- var import_node_path15 = __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);
182
33
 
183
34
  // src/core/config.ts
184
- var import_promises2 = require("fs/promises");
185
- var import_node_path4 = __toESM(require("path"), 1);
35
+ var import_promises = require("fs/promises");
36
+ var import_node_path = __toESM(require("path"), 1);
186
37
  var import_yaml = require("yaml");
187
38
  var defaultConfig = {
188
39
  paths: {
@@ -213,7 +64,7 @@ var defaultConfig = {
213
64
  testFileGlobs: [],
214
65
  testFileExcludeGlobs: [],
215
66
  scNoTestSeverity: "error",
216
- allowOrphanContracts: false,
67
+ orphanContractsPolicy: "error",
217
68
  unknownContractIdSeverity: "error"
218
69
  }
219
70
  },
@@ -222,14 +73,34 @@ var defaultConfig = {
222
73
  }
223
74
  };
224
75
  function getConfigPath(root) {
225
- return import_node_path4.default.join(root, "qfai.config.yaml");
76
+ return import_node_path.default.join(root, "qfai.config.yaml");
77
+ }
78
+ async function findConfigRoot(startDir) {
79
+ const resolvedStart = import_node_path.default.resolve(startDir);
80
+ let current = resolvedStart;
81
+ while (true) {
82
+ const configPath = getConfigPath(current);
83
+ if (await exists(configPath)) {
84
+ return { root: current, configPath, found: true };
85
+ }
86
+ const parent = import_node_path.default.dirname(current);
87
+ if (parent === current) {
88
+ break;
89
+ }
90
+ current = parent;
91
+ }
92
+ return {
93
+ root: resolvedStart,
94
+ configPath: getConfigPath(resolvedStart),
95
+ found: false
96
+ };
226
97
  }
227
98
  async function loadConfig(root) {
228
99
  const configPath = getConfigPath(root);
229
100
  const issues = [];
230
101
  let parsed;
231
102
  try {
232
- const raw = await (0, import_promises2.readFile)(configPath, "utf-8");
103
+ const raw = await (0, import_promises.readFile)(configPath, "utf-8");
233
104
  parsed = (0, import_yaml.parse)(raw);
234
105
  } catch (error2) {
235
106
  if (isMissingFile(error2)) {
@@ -242,7 +113,7 @@ async function loadConfig(root) {
242
113
  return { config: normalized, issues, configPath };
243
114
  }
244
115
  function resolvePath(root, config, key) {
245
- return import_node_path4.default.resolve(root, config.paths[key]);
116
+ return import_node_path.default.resolve(root, config.paths[key]);
246
117
  }
247
118
  function normalizeConfig(raw, configPath, issues) {
248
119
  if (!isRecord(raw)) {
@@ -413,10 +284,10 @@ function normalizeValidation(raw, configPath, issues) {
413
284
  configPath,
414
285
  issues
415
286
  ),
416
- allowOrphanContracts: readBoolean(
417
- traceabilityRaw?.allowOrphanContracts,
418
- base.traceability.allowOrphanContracts,
419
- "validation.traceability.allowOrphanContracts",
287
+ orphanContractsPolicy: readOrphanContractsPolicy(
288
+ traceabilityRaw?.orphanContractsPolicy,
289
+ base.traceability.orphanContractsPolicy,
290
+ "validation.traceability.orphanContractsPolicy",
420
291
  configPath,
421
292
  issues
422
293
  ),
@@ -512,6 +383,20 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
512
383
  }
513
384
  return fallback;
514
385
  }
386
+ function readOrphanContractsPolicy(value, fallback, label, configPath, issues) {
387
+ if (value === "error" || value === "warning" || value === "allow") {
388
+ return value;
389
+ }
390
+ if (value !== void 0) {
391
+ issues.push(
392
+ configIssue(
393
+ configPath,
394
+ `${label} \u306F error|warning|allow \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
395
+ )
396
+ );
397
+ }
398
+ return fallback;
399
+ }
515
400
  function configIssue(file, message) {
516
401
  return {
517
402
  code: "QFAI_CONFIG_INVALID",
@@ -527,6 +412,14 @@ function isMissingFile(error2) {
527
412
  }
528
413
  return false;
529
414
  }
415
+ async function exists(target) {
416
+ try {
417
+ await (0, import_promises.access)(target);
418
+ return true;
419
+ } catch {
420
+ return false;
421
+ }
422
+ }
530
423
  function formatError(error2) {
531
424
  if (error2 instanceof Error) {
532
425
  return error2.message;
@@ -537,20 +430,12 @@ function isRecord(value) {
537
430
  return value !== null && typeof value === "object" && !Array.isArray(value);
538
431
  }
539
432
 
540
- // src/core/report.ts
541
- var import_promises15 = require("fs/promises");
542
- var import_node_path14 = __toESM(require("path"), 1);
543
-
544
- // src/core/contractIndex.ts
545
- var import_promises6 = require("fs/promises");
546
- var import_node_path7 = __toESM(require("path"), 1);
547
-
548
433
  // src/core/discovery.ts
549
- var import_promises5 = require("fs/promises");
434
+ var import_promises4 = require("fs/promises");
550
435
 
551
436
  // src/core/fs.ts
552
- var import_promises3 = require("fs/promises");
553
- var import_node_path5 = __toESM(require("path"), 1);
437
+ var import_promises2 = require("fs/promises");
438
+ var import_node_path2 = __toESM(require("path"), 1);
554
439
  var import_fast_glob = __toESM(require("fast-glob"), 1);
555
440
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
556
441
  "node_modules",
@@ -586,9 +471,9 @@ async function collectFilesByGlobs(root, options) {
586
471
  });
587
472
  }
588
473
  async function walk(base, current, ignoreDirs, extensions, out) {
589
- const items = await (0, import_promises3.readdir)(current, { withFileTypes: true });
474
+ const items = await (0, import_promises2.readdir)(current, { withFileTypes: true });
590
475
  for (const item of items) {
591
- const fullPath = import_node_path5.default.join(current, item.name);
476
+ const fullPath = import_node_path2.default.join(current, item.name);
592
477
  if (item.isDirectory()) {
593
478
  if (ignoreDirs.has(item.name)) {
594
479
  continue;
@@ -598,7 +483,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
598
483
  }
599
484
  if (item.isFile()) {
600
485
  if (extensions.length > 0) {
601
- const ext = import_node_path5.default.extname(item.name).toLowerCase();
486
+ const ext = import_node_path2.default.extname(item.name).toLowerCase();
602
487
  if (!extensions.includes(ext)) {
603
488
  continue;
604
489
  }
@@ -609,7 +494,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
609
494
  }
610
495
  async function exists2(target) {
611
496
  try {
612
- await (0, import_promises3.access)(target);
497
+ await (0, import_promises2.access)(target);
613
498
  return true;
614
499
  } catch {
615
500
  return false;
@@ -617,23 +502,23 @@ async function exists2(target) {
617
502
  }
618
503
 
619
504
  // src/core/specLayout.ts
620
- var import_promises4 = require("fs/promises");
621
- var import_node_path6 = __toESM(require("path"), 1);
505
+ var import_promises3 = require("fs/promises");
506
+ var import_node_path3 = __toESM(require("path"), 1);
622
507
  var SPEC_DIR_RE = /^spec-\d{4}$/;
623
508
  async function collectSpecEntries(specsRoot) {
624
509
  const dirs = await listSpecDirs(specsRoot);
625
510
  const entries = dirs.map((dir) => ({
626
511
  dir,
627
- specPath: import_node_path6.default.join(dir, "spec.md"),
628
- deltaPath: import_node_path6.default.join(dir, "delta.md"),
629
- scenarioPath: import_node_path6.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")
630
515
  }));
631
516
  return entries.sort((a, b) => a.dir.localeCompare(b.dir));
632
517
  }
633
518
  async function listSpecDirs(specsRoot) {
634
519
  try {
635
- const items = await (0, import_promises4.readdir)(specsRoot, { withFileTypes: true });
636
- return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path6.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));
637
522
  } catch (error2) {
638
523
  if (isMissingFileError(error2)) {
639
524
  return [];
@@ -689,314 +574,65 @@ async function filterExisting(files) {
689
574
  }
690
575
  async function exists3(target) {
691
576
  try {
692
- await (0, import_promises5.access)(target);
577
+ await (0, import_promises4.access)(target);
693
578
  return true;
694
579
  } catch {
695
580
  return false;
696
581
  }
697
582
  }
698
583
 
699
- // src/core/contractsDecl.ts
700
- var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
701
- var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
702
- function extractDeclaredContractIds(text) {
703
- const ids = [];
704
- for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
705
- const id = match[1];
706
- if (id) {
707
- ids.push(id);
708
- }
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;
709
589
  }
710
- return ids;
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);
711
598
  }
712
- function stripContractDeclarationLines(text) {
713
- return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
599
+ function toPosixPath(value) {
600
+ return value.replace(/\\/g, "/");
714
601
  }
715
602
 
716
- // src/core/contractIndex.ts
717
- async function buildContractIndex(root, config) {
718
- const contractsRoot = resolvePath(root, config, "contractsDir");
719
- const uiRoot = import_node_path7.default.join(contractsRoot, "ui");
720
- const apiRoot = import_node_path7.default.join(contractsRoot, "api");
721
- const dbRoot = import_node_path7.default.join(contractsRoot, "db");
722
- const [uiFiles, apiFiles, dbFiles] = await Promise.all([
723
- collectUiContractFiles(uiRoot),
724
- collectApiContractFiles(apiRoot),
725
- collectDbContractFiles(dbRoot)
726
- ]);
727
- const index = {
728
- ids: /* @__PURE__ */ new Set(),
729
- idToFiles: /* @__PURE__ */ new Map(),
730
- files: { ui: uiFiles, api: apiFiles, db: dbFiles }
731
- };
732
- await indexContractFiles(uiFiles, index);
733
- await indexContractFiles(apiFiles, index);
734
- await indexContractFiles(dbFiles, index);
735
- return index;
736
- }
737
- async function indexContractFiles(files, index) {
738
- for (const file of files) {
739
- const text = await (0, import_promises6.readFile)(file, "utf-8");
740
- extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
603
+ // src/core/traceability.ts
604
+ var import_promises5 = require("fs/promises");
605
+ var import_node_path5 = __toESM(require("path"), 1);
606
+
607
+ // src/core/gherkin/parse.ts
608
+ var import_gherkin = require("@cucumber/gherkin");
609
+ var import_node_crypto = require("crypto");
610
+ function parseGherkin(source, uri) {
611
+ const errors = [];
612
+ const uuidFn = () => (0, import_node_crypto.randomUUID)();
613
+ const builder = new import_gherkin.AstBuilder(uuidFn);
614
+ const matcher = new import_gherkin.GherkinClassicTokenMatcher();
615
+ const parser = new import_gherkin.Parser(builder, matcher);
616
+ try {
617
+ const gherkinDocument = parser.parse(source);
618
+ gherkinDocument.uri = uri;
619
+ return { gherkinDocument, errors };
620
+ } catch (error2) {
621
+ errors.push(formatError2(error2));
622
+ return { gherkinDocument: null, errors };
741
623
  }
742
624
  }
743
- function record(index, id, file) {
744
- index.ids.add(id);
745
- const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
746
- current.add(file);
747
- index.idToFiles.set(id, current);
748
- }
749
-
750
- // src/core/ids.ts
751
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
752
- var STRICT_ID_PATTERNS = {
753
- SPEC: /\bSPEC-\d{4}\b/g,
754
- BR: /\bBR-\d{4}\b/g,
755
- SC: /\bSC-\d{4}\b/g,
756
- UI: /\bUI-\d{4}\b/g,
757
- API: /\bAPI-\d{4}\b/g,
758
- DB: /\bDB-\d{4}\b/g,
759
- ADR: /\bADR-\d{4}\b/g
760
- };
761
- var LOOSE_ID_PATTERNS = {
762
- SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
763
- BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
764
- SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
765
- UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
766
- API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
767
- DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
768
- ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
769
- };
770
- function extractIds(text, prefix) {
771
- const pattern = STRICT_ID_PATTERNS[prefix];
772
- const matches = text.match(pattern);
773
- return unique(matches ?? []);
774
- }
775
- function extractAllIds(text) {
776
- const all = [];
777
- ID_PREFIXES.forEach((prefix) => {
778
- all.push(...extractIds(text, prefix));
779
- });
780
- return unique(all);
781
- }
782
- function extractInvalidIds(text, prefixes) {
783
- const invalid = [];
784
- for (const prefix of prefixes) {
785
- const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
786
- for (const candidate of candidates) {
787
- if (!isValidId(candidate, prefix)) {
788
- invalid.push(candidate);
789
- }
790
- }
791
- }
792
- return unique(invalid);
793
- }
794
- function unique(values) {
795
- return Array.from(new Set(values));
796
- }
797
- function isValidId(value, prefix) {
798
- const pattern = STRICT_ID_PATTERNS[prefix];
799
- const strict = new RegExp(pattern.source);
800
- return strict.test(value);
801
- }
802
-
803
- // src/core/parse/markdown.ts
804
- var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
805
- function parseHeadings(md) {
806
- const lines = md.split(/\r?\n/);
807
- const headings = [];
808
- for (let i = 0; i < lines.length; i++) {
809
- const line = lines[i] ?? "";
810
- const match = line.match(HEADING_RE);
811
- if (!match) continue;
812
- const levelToken = match[1];
813
- const title = match[2];
814
- if (!levelToken || !title) continue;
815
- headings.push({
816
- level: levelToken.length,
817
- title: title.trim(),
818
- line: i + 1
819
- });
820
- }
821
- return headings;
822
- }
823
- function extractH2Sections(md) {
824
- const lines = md.split(/\r?\n/);
825
- const headings = parseHeadings(md).filter((heading) => heading.level === 2);
826
- const sections = /* @__PURE__ */ new Map();
827
- for (let i = 0; i < headings.length; i++) {
828
- const current = headings[i];
829
- if (!current) continue;
830
- const next = headings[i + 1];
831
- const startLine = current.line + 1;
832
- const endLine = (next?.line ?? lines.length + 1) - 1;
833
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
834
- sections.set(current.title.trim(), {
835
- title: current.title.trim(),
836
- startLine,
837
- endLine,
838
- body
839
- });
840
- }
841
- return sections;
842
- }
843
-
844
- // src/core/parse/spec.ts
845
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
846
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
847
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
848
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
849
- var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
850
- var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
851
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
852
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
853
- function parseSpec(md, file) {
854
- const headings = parseHeadings(md);
855
- const h1 = headings.find((heading) => heading.level === 1);
856
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
857
- const sections = extractH2Sections(md);
858
- const sectionNames = new Set(Array.from(sections.keys()));
859
- const brSection = sections.get(BR_SECTION_TITLE);
860
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
861
- const startLine = brSection?.startLine ?? 1;
862
- const brs = [];
863
- const brsWithoutPriority = [];
864
- const brsWithInvalidPriority = [];
865
- for (let i = 0; i < brLines.length; i++) {
866
- const lineText = brLines[i] ?? "";
867
- const lineNumber = startLine + i;
868
- const validMatch = lineText.match(BR_LINE_RE);
869
- if (validMatch) {
870
- const id = validMatch[1];
871
- const priority = validMatch[2];
872
- const text = validMatch[3];
873
- if (!id || !priority || !text) continue;
874
- brs.push({
875
- id,
876
- priority,
877
- text: text.trim(),
878
- line: lineNumber
879
- });
880
- continue;
881
- }
882
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
883
- if (anyPriorityMatch) {
884
- const id = anyPriorityMatch[1];
885
- const priority = anyPriorityMatch[2];
886
- const text = anyPriorityMatch[3];
887
- if (!id || !priority || !text) continue;
888
- if (!VALID_PRIORITIES.has(priority)) {
889
- brsWithInvalidPriority.push({
890
- id,
891
- priority,
892
- text: text.trim(),
893
- line: lineNumber
894
- });
895
- }
896
- continue;
897
- }
898
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
899
- if (noPriorityMatch) {
900
- const id = noPriorityMatch[1];
901
- const text = noPriorityMatch[2];
902
- if (!id || !text) continue;
903
- brsWithoutPriority.push({
904
- id,
905
- text: text.trim(),
906
- line: lineNumber
907
- });
908
- }
909
- }
910
- const parsed = {
911
- file,
912
- sections: sectionNames,
913
- brs,
914
- brsWithoutPriority,
915
- brsWithInvalidPriority,
916
- contractRefs: parseContractRefs(md)
917
- };
918
- if (specId) {
919
- parsed.specId = specId;
920
- }
921
- return parsed;
922
- }
923
- function parseContractRefs(md) {
924
- const lines = [];
925
- for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
926
- lines.push((match[1] ?? "").trim());
927
- }
928
- const ids = [];
929
- const invalidTokens = [];
930
- let hasNone = false;
931
- for (const line of lines) {
932
- if (line.length === 0) {
933
- invalidTokens.push("(empty)");
934
- continue;
935
- }
936
- const tokens = line.split(",").map((token) => token.trim());
937
- for (const token of tokens) {
938
- if (token.length === 0) {
939
- invalidTokens.push("(empty)");
940
- continue;
941
- }
942
- if (token === "none") {
943
- hasNone = true;
944
- continue;
945
- }
946
- if (CONTRACT_REF_ID_RE.test(token)) {
947
- ids.push(token);
948
- continue;
949
- }
950
- invalidTokens.push(token);
951
- }
952
- }
953
- return {
954
- lines,
955
- ids: unique2(ids),
956
- invalidTokens: unique2(invalidTokens),
957
- hasNone
958
- };
959
- }
960
- function unique2(values) {
961
- return Array.from(new Set(values));
962
- }
963
-
964
- // src/core/traceability.ts
965
- var import_promises7 = require("fs/promises");
966
- var import_node_path8 = __toESM(require("path"), 1);
967
-
968
- // src/core/gherkin/parse.ts
969
- var import_gherkin = require("@cucumber/gherkin");
970
- var import_node_crypto = require("crypto");
971
- function parseGherkin(source, uri) {
972
- const errors = [];
973
- const uuidFn = () => (0, import_node_crypto.randomUUID)();
974
- const builder = new import_gherkin.AstBuilder(uuidFn);
975
- const matcher = new import_gherkin.GherkinClassicTokenMatcher();
976
- const parser = new import_gherkin.Parser(builder, matcher);
977
- try {
978
- const gherkinDocument = parser.parse(source);
979
- gherkinDocument.uri = uri;
980
- return { gherkinDocument, errors };
981
- } catch (error2) {
982
- errors.push(formatError2(error2));
983
- return { gherkinDocument: null, errors };
984
- }
985
- }
986
- function formatError2(error2) {
987
- if (error2 instanceof Error) {
988
- return error2.message;
989
- }
990
- return String(error2);
625
+ function formatError2(error2) {
626
+ if (error2 instanceof Error) {
627
+ return error2.message;
628
+ }
629
+ return String(error2);
991
630
  }
992
631
 
993
632
  // src/core/scenarioModel.ts
994
633
  var SPEC_TAG_RE = /^SPEC-\d{4}$/;
995
634
  var SC_TAG_RE = /^SC-\d{4}$/;
996
635
  var BR_TAG_RE = /^BR-\d{4}$/;
997
- var UI_TAG_RE = /^UI-\d{4}$/;
998
- var API_TAG_RE = /^API-\d{4}$/;
999
- var DB_TAG_RE = /^DB-\d{4}$/;
1000
636
  function parseScenarioDocument(text, uri) {
1001
637
  const { gherkinDocument, errors } = parseGherkin(text, uri);
1002
638
  if (!gherkinDocument) {
@@ -1021,31 +657,21 @@ function parseScenarioDocument(text, uri) {
1021
657
  errors
1022
658
  };
1023
659
  }
1024
- function buildScenarioAtoms(document) {
660
+ function buildScenarioAtoms(document, contractIds = []) {
661
+ const uniqueContractIds = unique(contractIds).sort(
662
+ (a, b) => a.localeCompare(b)
663
+ );
1025
664
  return document.scenarios.map((scenario) => {
1026
665
  const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1027
666
  const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1028
- const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1029
- const contractIds = /* @__PURE__ */ new Set();
1030
- scenario.tags.forEach((tag) => {
1031
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
1032
- contractIds.add(tag);
1033
- }
1034
- });
1035
- for (const step of scenario.steps) {
1036
- for (const text of collectStepTexts(step)) {
1037
- extractIds(text, "UI").forEach((id) => contractIds.add(id));
1038
- extractIds(text, "API").forEach((id) => contractIds.add(id));
1039
- extractIds(text, "DB").forEach((id) => contractIds.add(id));
1040
- }
1041
- }
667
+ const brIds = unique(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1042
668
  const atom = {
1043
669
  uri: document.uri,
1044
670
  featureName: document.featureName ?? "",
1045
671
  scenarioName: scenario.name,
1046
672
  kind: scenario.kind,
1047
673
  brIds,
1048
- contractIds: Array.from(contractIds).sort()
674
+ contractIds: uniqueContractIds
1049
675
  };
1050
676
  if (scenario.line !== void 0) {
1051
677
  atom.line = scenario.line;
@@ -1098,24 +724,7 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
1098
724
  function collectTagNames(tags) {
1099
725
  return tags.map((tag) => tag.name.replace(/^@/, ""));
1100
726
  }
1101
- function collectStepTexts(step) {
1102
- const texts = [];
1103
- if (step.text) {
1104
- texts.push(step.text);
1105
- }
1106
- if (step.docString?.content) {
1107
- texts.push(step.docString.content);
1108
- }
1109
- if (step.dataTable?.rows) {
1110
- for (const row of step.dataTable.rows) {
1111
- for (const cell of row.cells) {
1112
- texts.push(cell.value);
1113
- }
1114
- }
1115
- }
1116
- return texts;
1117
- }
1118
- function unique3(values) {
727
+ function unique(values) {
1119
728
  return Array.from(new Set(values));
1120
729
  }
1121
730
 
@@ -1145,7 +754,7 @@ function extractAnnotatedScIds(text) {
1145
754
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
1146
755
  const scIds = /* @__PURE__ */ new Set();
1147
756
  for (const file of scenarioFiles) {
1148
- const text = await (0, import_promises7.readFile)(file, "utf-8");
757
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
1149
758
  const { document, errors } = parseScenarioDocument(text, file);
1150
759
  if (!document || errors.length > 0) {
1151
760
  continue;
@@ -1163,7 +772,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
1163
772
  async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
1164
773
  const sources = /* @__PURE__ */ new Map();
1165
774
  for (const file of scenarioFiles) {
1166
- const text = await (0, import_promises7.readFile)(file, "utf-8");
775
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
1167
776
  const { document, errors } = parseScenarioDocument(text, file);
1168
777
  if (!document || errors.length > 0) {
1169
778
  continue;
@@ -1216,10 +825,10 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1216
825
  };
1217
826
  }
1218
827
  const normalizedFiles = Array.from(
1219
- new Set(files.map((file) => import_node_path8.default.normalize(file)))
828
+ new Set(files.map((file) => import_node_path5.default.normalize(file)))
1220
829
  );
1221
830
  for (const file of normalizedFiles) {
1222
- const text = await (0, import_promises7.readFile)(file, "utf-8");
831
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
1223
832
  const scIds = extractAnnotatedScIds(text);
1224
833
  if (scIds.length === 0) {
1225
834
  continue;
@@ -1229,85 +838,772 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1229
838
  current.add(file);
1230
839
  refs.set(scId, current);
1231
840
  }
1232
- }
1233
- return {
1234
- refs,
1235
- scan: {
1236
- globs: normalizedGlobs,
1237
- excludeGlobs: mergedExcludeGlobs,
1238
- matchedFileCount: normalizedFiles.length
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)");
1442
+ continue;
1443
+ }
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);
1459
+ }
1460
+ }
1461
+ return {
1462
+ lines,
1463
+ ids: unique3(ids),
1464
+ invalidTokens: unique3(invalidTokens),
1465
+ hasNone
1466
+ };
1467
+ }
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
+ );
1474
+ }
1475
+ function unique3(values) {
1476
+ return Array.from(new Set(values));
1477
+ }
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;
1498
+ }
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
+ });
1516
+ }
1517
+ return sections;
1518
+ }
1519
+
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;
1239
1571
  }
1240
- };
1241
- }
1242
- function buildScCoverage(scIds, refs) {
1243
- const sortedScIds = toSortedArray(scIds);
1244
- const refsRecord = {};
1245
- const missingIds = [];
1246
- let covered = 0;
1247
- for (const scId of sortedScIds) {
1248
- const files = refs.get(scId);
1249
- const sortedFiles = files ? toSortedArray(files) : [];
1250
- refsRecord[scId] = sortedFiles;
1251
- if (sortedFiles.length === 0) {
1252
- missingIds.push(scId);
1253
- } else {
1254
- covered += 1;
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
+ });
1255
1582
  }
1256
1583
  }
1257
- return {
1258
- total: sortedScIds.length,
1259
- covered,
1260
- missing: missingIds.length,
1261
- missingIds,
1262
- refs: refsRecord
1584
+ const parsed = {
1585
+ file,
1586
+ sections: sectionNames,
1587
+ brs,
1588
+ brsWithoutPriority,
1589
+ brsWithInvalidPriority,
1590
+ contractRefs: parseContractRefs(md)
1263
1591
  };
1264
- }
1265
- function toSortedArray(values) {
1266
- return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
1267
- }
1268
- function normalizeGlobs(globs) {
1269
- return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1270
- }
1271
- function formatError3(error2) {
1272
- if (error2 instanceof Error) {
1273
- return error2.message;
1274
- }
1275
- return String(error2);
1276
- }
1277
-
1278
- // src/core/version.ts
1279
- var import_promises8 = require("fs/promises");
1280
- var import_node_path9 = __toESM(require("path"), 1);
1281
- var import_node_url2 = require("url");
1282
- async function resolveToolVersion() {
1283
- if ("0.5.0".length > 0) {
1284
- return "0.5.0";
1285
- }
1286
- try {
1287
- const packagePath = resolvePackageJsonPath();
1288
- const raw = await (0, import_promises8.readFile)(packagePath, "utf-8");
1289
- const parsed = JSON.parse(raw);
1290
- const version = typeof parsed.version === "string" ? parsed.version : "";
1291
- return version.length > 0 ? version : "unknown";
1292
- } catch {
1293
- return "unknown";
1592
+ if (specId) {
1593
+ parsed.specId = specId;
1294
1594
  }
1295
- }
1296
- function resolvePackageJsonPath() {
1297
- const base = __filename;
1298
- const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
1299
- return import_node_path9.default.resolve(import_node_path9.default.dirname(basePath), "../../package.json");
1595
+ return parsed;
1300
1596
  }
1301
1597
 
1302
1598
  // src/core/validators/contracts.ts
1303
- var import_promises9 = require("fs/promises");
1304
- var import_node_path11 = __toESM(require("path"), 1);
1599
+ var import_promises11 = require("fs/promises");
1600
+ var import_node_path14 = __toESM(require("path"), 1);
1305
1601
 
1306
1602
  // src/core/contracts.ts
1307
- var import_node_path10 = __toESM(require("path"), 1);
1603
+ var import_node_path13 = __toESM(require("path"), 1);
1308
1604
  var import_yaml2 = require("yaml");
1309
1605
  function parseStructuredContract(file, text) {
1310
- const ext = import_node_path10.default.extname(file).toLowerCase();
1606
+ const ext = import_node_path13.default.extname(file).toLowerCase();
1311
1607
  if (ext === ".json") {
1312
1608
  return JSON.parse(text);
1313
1609
  }
@@ -1327,9 +1623,9 @@ var SQL_DANGEROUS_PATTERNS = [
1327
1623
  async function validateContracts(root, config) {
1328
1624
  const issues = [];
1329
1625
  const contractsRoot = resolvePath(root, config, "contractsDir");
1330
- issues.push(...await validateUiContracts(import_node_path11.default.join(contractsRoot, "ui")));
1331
- issues.push(...await validateApiContracts(import_node_path11.default.join(contractsRoot, "api")));
1332
- issues.push(...await validateDbContracts(import_node_path11.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")));
1333
1629
  const contractIndex = await buildContractIndex(root, config);
1334
1630
  issues.push(...validateDuplicateContractIds(contractIndex));
1335
1631
  return issues;
@@ -1349,7 +1645,7 @@ async function validateUiContracts(uiRoot) {
1349
1645
  }
1350
1646
  const issues = [];
1351
1647
  for (const file of files) {
1352
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1648
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1353
1649
  const invalidIds = extractInvalidIds(text, [
1354
1650
  "SPEC",
1355
1651
  "BR",
@@ -1404,7 +1700,7 @@ async function validateApiContracts(apiRoot) {
1404
1700
  }
1405
1701
  const issues = [];
1406
1702
  for (const file of files) {
1407
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1703
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1408
1704
  const invalidIds = extractInvalidIds(text, [
1409
1705
  "SPEC",
1410
1706
  "BR",
@@ -1472,7 +1768,7 @@ async function validateDbContracts(dbRoot) {
1472
1768
  }
1473
1769
  const issues = [];
1474
1770
  for (const file of files) {
1475
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1771
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1476
1772
  const invalidIds = extractInvalidIds(text, [
1477
1773
  "SPEC",
1478
1774
  "BR",
@@ -1611,8 +1907,8 @@ function issue(code, message, severity, file, rule, refs) {
1611
1907
  }
1612
1908
 
1613
1909
  // src/core/validators/delta.ts
1614
- var import_promises10 = require("fs/promises");
1615
- var import_node_path12 = __toESM(require("path"), 1);
1910
+ var import_promises12 = require("fs/promises");
1911
+ var import_node_path15 = __toESM(require("path"), 1);
1616
1912
  var SECTION_RE = /^##\s+変更区分/m;
1617
1913
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1618
1914
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1626,10 +1922,10 @@ async function validateDeltas(root, config) {
1626
1922
  }
1627
1923
  const issues = [];
1628
1924
  for (const pack of packs) {
1629
- const deltaPath = import_node_path12.default.join(pack, "delta.md");
1925
+ const deltaPath = import_node_path15.default.join(pack, "delta.md");
1630
1926
  let text;
1631
1927
  try {
1632
- text = await (0, import_promises10.readFile)(deltaPath, "utf-8");
1928
+ text = await (0, import_promises12.readFile)(deltaPath, "utf-8");
1633
1929
  } catch (error2) {
1634
1930
  if (isMissingFileError2(error2)) {
1635
1931
  issues.push(
@@ -1701,8 +1997,8 @@ function issue2(code, message, severity, file, rule, refs) {
1701
1997
  }
1702
1998
 
1703
1999
  // src/core/validators/ids.ts
1704
- var import_promises11 = require("fs/promises");
1705
- var import_node_path13 = __toESM(require("path"), 1);
2000
+ var import_promises13 = require("fs/promises");
2001
+ var import_node_path16 = __toESM(require("path"), 1);
1706
2002
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1707
2003
  async function validateDefinedIds(root, config) {
1708
2004
  const issues = [];
@@ -1737,7 +2033,7 @@ async function validateDefinedIds(root, config) {
1737
2033
  }
1738
2034
  async function collectSpecDefinitionIds(files, out) {
1739
2035
  for (const file of files) {
1740
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2036
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
1741
2037
  const parsed = parseSpec(text, file);
1742
2038
  if (parsed.specId) {
1743
2039
  recordId(out, parsed.specId, file);
@@ -1747,7 +2043,7 @@ async function collectSpecDefinitionIds(files, out) {
1747
2043
  }
1748
2044
  async function collectScenarioDefinitionIds(files, out) {
1749
2045
  for (const file of files) {
1750
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2046
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
1751
2047
  const { document, errors } = parseScenarioDocument(text, file);
1752
2048
  if (!document || errors.length > 0) {
1753
2049
  continue;
@@ -1768,7 +2064,7 @@ function recordId(out, id, file) {
1768
2064
  }
1769
2065
  function formatFileList(files, root) {
1770
2066
  return files.map((file) => {
1771
- const relative = import_node_path13.default.relative(root, file);
2067
+ const relative = import_node_path16.default.relative(root, file);
1772
2068
  return relative.length > 0 ? relative : file;
1773
2069
  }).join(", ");
1774
2070
  }
@@ -1791,7 +2087,7 @@ function issue3(code, message, severity, file, rule, refs) {
1791
2087
  }
1792
2088
 
1793
2089
  // src/core/validators/scenario.ts
1794
- var import_promises12 = require("fs/promises");
2090
+ var import_promises14 = require("fs/promises");
1795
2091
  var GIVEN_PATTERN = /\bGiven\b/;
1796
2092
  var WHEN_PATTERN = /\bWhen\b/;
1797
2093
  var THEN_PATTERN = /\bThen\b/;
@@ -1817,7 +2113,7 @@ async function validateScenarios(root, config) {
1817
2113
  for (const entry of entries) {
1818
2114
  let text;
1819
2115
  try {
1820
- text = await (0, import_promises12.readFile)(entry.scenarioPath, "utf-8");
2116
+ text = await (0, import_promises14.readFile)(entry.scenarioPath, "utf-8");
1821
2117
  } catch (error2) {
1822
2118
  if (isMissingFileError3(error2)) {
1823
2119
  issues.push(
@@ -1987,7 +2283,7 @@ function isMissingFileError3(error2) {
1987
2283
  }
1988
2284
 
1989
2285
  // src/core/validators/spec.ts
1990
- var import_promises13 = require("fs/promises");
2286
+ var import_promises15 = require("fs/promises");
1991
2287
  async function validateSpecs(root, config) {
1992
2288
  const specsRoot = resolvePath(root, config, "specsDir");
1993
2289
  const entries = await collectSpecEntries(specsRoot);
@@ -2008,7 +2304,7 @@ async function validateSpecs(root, config) {
2008
2304
  for (const entry of entries) {
2009
2305
  let text;
2010
2306
  try {
2011
- text = await (0, import_promises13.readFile)(entry.specPath, "utf-8");
2307
+ text = await (0, import_promises15.readFile)(entry.specPath, "utf-8");
2012
2308
  } catch (error2) {
2013
2309
  if (isMissingFileError4(error2)) {
2014
2310
  issues.push(
@@ -2157,7 +2453,7 @@ function isMissingFileError4(error2) {
2157
2453
  }
2158
2454
 
2159
2455
  // src/core/validators/traceability.ts
2160
- var import_promises14 = require("fs/promises");
2456
+ var import_promises16 = require("fs/promises");
2161
2457
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
2162
2458
  var BR_TAG_RE2 = /^BR-\d{4}$/;
2163
2459
  async function validateTraceability(root, config) {
@@ -2177,7 +2473,7 @@ async function validateTraceability(root, config) {
2177
2473
  const contractIndex = await buildContractIndex(root, config);
2178
2474
  const contractIds = contractIndex.ids;
2179
2475
  for (const file of specFiles) {
2180
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2476
+ const text = await (0, import_promises16.readFile)(file, "utf-8");
2181
2477
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2182
2478
  const parsed = parseSpec(text, file);
2183
2479
  if (parsed.specId) {
@@ -2205,7 +2501,7 @@ async function validateTraceability(root, config) {
2205
2501
  if (contractRefs.hasNone && contractRefs.ids.length > 0) {
2206
2502
  issues.push(
2207
2503
  issue6(
2208
- "QFAI-TRACE-021",
2504
+ "QFAI-TRACE-023",
2209
2505
  "Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2210
2506
  "error",
2211
2507
  file,
@@ -2237,7 +2533,7 @@ async function validateTraceability(root, config) {
2237
2533
  if (unknownContractIds.length > 0) {
2238
2534
  issues.push(
2239
2535
  issue6(
2240
- "QFAI-TRACE-021",
2536
+ "QFAI-TRACE-024",
2241
2537
  `Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2242
2538
  ", "
2243
2539
  )}`,
@@ -2250,13 +2546,64 @@ async function validateTraceability(root, config) {
2250
2546
  }
2251
2547
  }
2252
2548
  for (const file of scenarioFiles) {
2253
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2549
+ const text = await (0, import_promises16.readFile)(file, "utf-8");
2254
2550
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2551
+ const scenarioContractRefs = parseContractRefs(text, {
2552
+ allowCommentPrefix: true
2553
+ });
2554
+ if (scenarioContractRefs.lines.length === 0) {
2555
+ issues.push(
2556
+ issue6(
2557
+ "QFAI-TRACE-031",
2558
+ "Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2559
+ "error",
2560
+ file,
2561
+ "traceability.scenarioContractRefRequired"
2562
+ )
2563
+ );
2564
+ } else {
2565
+ if (scenarioContractRefs.hasNone && scenarioContractRefs.ids.length > 0) {
2566
+ issues.push(
2567
+ issue6(
2568
+ "QFAI-TRACE-033",
2569
+ "Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2570
+ "error",
2571
+ file,
2572
+ "traceability.scenarioContractRefFormat"
2573
+ )
2574
+ );
2575
+ }
2576
+ if (scenarioContractRefs.invalidTokens.length > 0) {
2577
+ issues.push(
2578
+ issue6(
2579
+ "QFAI-TRACE-032",
2580
+ `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
2581
+ ", "
2582
+ )}`,
2583
+ "error",
2584
+ file,
2585
+ "traceability.scenarioContractRefFormat",
2586
+ scenarioContractRefs.invalidTokens
2587
+ )
2588
+ );
2589
+ }
2590
+ }
2255
2591
  const { document, errors } = parseScenarioDocument(text, file);
2256
2592
  if (!document || errors.length > 0) {
2257
2593
  continue;
2258
2594
  }
2259
- const atoms = buildScenarioAtoms(document);
2595
+ if (document.scenarios.length !== 1) {
2596
+ issues.push(
2597
+ issue6(
2598
+ "QFAI-TRACE-030",
2599
+ `Scenario \u30D5\u30A1\u30A4\u30EB\u306F 1\u30D5\u30A1\u30A4\u30EB=1\u30B7\u30CA\u30EA\u30AA\u3067\u3059\u3002\u73FE\u5728: ${document.scenarios.length}\u4EF6 (file=${file})`,
2600
+ "error",
2601
+ file,
2602
+ "traceability.scenarioOnePerFile"
2603
+ )
2604
+ );
2605
+ }
2606
+ const atoms = buildScenarioAtoms(document, scenarioContractRefs.ids);
2260
2607
  const scIdsInFile = /* @__PURE__ */ new Set();
2261
2608
  for (const [index, scenario] of document.scenarios.entries()) {
2262
2609
  const atom = atoms[index];
@@ -2401,7 +2748,7 @@ async function validateTraceability(root, config) {
2401
2748
  if (orphanBrIds.length > 0) {
2402
2749
  issues.push(
2403
2750
  issue6(
2404
- "QFAI_TRACE_BR_ORPHAN",
2751
+ "QFAI-TRACE-009",
2405
2752
  `BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
2406
2753
  "error",
2407
2754
  specsRoot,
@@ -2471,17 +2818,19 @@ async function validateTraceability(root, config) {
2471
2818
  );
2472
2819
  }
2473
2820
  }
2474
- if (!config.validation.traceability.allowOrphanContracts) {
2821
+ const orphanPolicy = config.validation.traceability.orphanContractsPolicy;
2822
+ if (orphanPolicy !== "allow") {
2475
2823
  if (contractIds.size > 0) {
2476
2824
  const orphanContracts = Array.from(contractIds).filter(
2477
2825
  (id) => !specContractIds.has(id)
2478
2826
  );
2479
2827
  if (orphanContracts.length > 0) {
2828
+ const severity = orphanPolicy === "warning" ? "warning" : "error";
2480
2829
  issues.push(
2481
2830
  issue6(
2482
2831
  "QFAI-TRACE-022",
2483
2832
  `\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2484
- "error",
2833
+ severity,
2485
2834
  specsRoot,
2486
2835
  "traceability.contractCoverage",
2487
2836
  orphanContracts
@@ -2519,7 +2868,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2519
2868
  const pattern = buildIdPattern(Array.from(upstreamIds));
2520
2869
  let found = false;
2521
2870
  for (const file of targetFiles) {
2522
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2871
+ const text = await (0, import_promises16.readFile)(file, "utf-8");
2523
2872
  if (pattern.test(text)) {
2524
2873
  found = true;
2525
2874
  break;
@@ -2606,16 +2955,17 @@ function countIssues(issues) {
2606
2955
  // src/core/report.ts
2607
2956
  var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2608
2957
  async function createReportData(root, validation, configResult) {
2609
- const resolved = configResult ?? await loadConfig(root);
2958
+ const resolvedRoot = import_node_path17.default.resolve(root);
2959
+ const resolved = configResult ?? await loadConfig(resolvedRoot);
2610
2960
  const config = resolved.config;
2611
2961
  const configPath = resolved.configPath;
2612
- const specsRoot = resolvePath(root, config, "specsDir");
2613
- const contractsRoot = resolvePath(root, config, "contractsDir");
2614
- const apiRoot = import_node_path14.default.join(contractsRoot, "api");
2615
- const uiRoot = import_node_path14.default.join(contractsRoot, "ui");
2616
- const dbRoot = import_node_path14.default.join(contractsRoot, "db");
2617
- const srcRoot = resolvePath(root, config, "srcDir");
2618
- const testsRoot = resolvePath(root, config, "testsDir");
2962
+ const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
2963
+ const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
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");
2967
+ const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
2968
+ const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
2619
2969
  const specFiles = await collectSpecFiles(specsRoot);
2620
2970
  const scenarioFiles = await collectScenarioFiles(specsRoot);
2621
2971
  const {
@@ -2623,15 +2973,15 @@ async function createReportData(root, validation, configResult) {
2623
2973
  ui: uiFiles,
2624
2974
  db: dbFiles
2625
2975
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
2626
- const contractIndex = await buildContractIndex(root, config);
2976
+ const contractIndex = await buildContractIndex(resolvedRoot, config);
2627
2977
  const contractIdList = Array.from(contractIndex.ids);
2628
2978
  const specContractRefs = await collectSpecContractRefs(
2629
2979
  specFiles,
2630
2980
  contractIdList
2631
2981
  );
2632
2982
  const referencedContracts = /* @__PURE__ */ new Set();
2633
- for (const ids of specContractRefs.specToContractIds.values()) {
2634
- ids.forEach((id) => referencedContracts.add(id));
2983
+ for (const entry of specContractRefs.specToContracts.values()) {
2984
+ entry.ids.forEach((id) => referencedContracts.add(id));
2635
2985
  }
2636
2986
  const referencedContractCount = contractIdList.filter(
2637
2987
  (id) => referencedContracts.has(id)
@@ -2640,8 +2990,8 @@ async function createReportData(root, validation, configResult) {
2640
2990
  (id) => !referencedContracts.has(id)
2641
2991
  ).length;
2642
2992
  const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
2643
- const specToContractIdsRecord = mapToSortedRecord(
2644
- specContractRefs.specToContractIds
2993
+ const specToContractsRecord = mapToSpecContractRecord(
2994
+ specContractRefs.specToContracts
2645
2995
  );
2646
2996
  const idsByPrefix = await collectIds([
2647
2997
  ...specFiles,
@@ -2659,24 +3009,28 @@ async function createReportData(root, validation, configResult) {
2659
3009
  srcRoot,
2660
3010
  testsRoot
2661
3011
  );
2662
- const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2663
- const scRefsResult = await collectScTestReferences(
2664
- root,
2665
- config.validation.traceability.testFileGlobs,
2666
- config.validation.traceability.testFileExcludeGlobs
3012
+ const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
3013
+ const normalizedValidation = normalizeValidationResult(
3014
+ resolvedRoot,
3015
+ resolvedValidationRaw
2667
3016
  );
2668
- const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2669
- const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
3017
+ const scCoverage = normalizedValidation.traceability.sc;
3018
+ const testFiles = normalizedValidation.traceability.testFiles;
2670
3019
  const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2671
- const scSourceRecord = mapToSortedRecord(scSources);
2672
- const resolvedValidation = validation ?? await validateProject(root, resolved);
3020
+ const scSourceRecord = mapToSortedRecord(
3021
+ normalizeScSources(resolvedRoot, scSources)
3022
+ );
2673
3023
  const version = await resolveToolVersion();
3024
+ const reportFormatVersion = 1;
3025
+ const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
3026
+ const displayConfigPath = toRelativePath(resolvedRoot, configPath);
2674
3027
  return {
2675
3028
  tool: "qfai",
2676
3029
  version,
3030
+ reportFormatVersion,
2677
3031
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2678
- root,
2679
- configPath,
3032
+ root: displayRoot,
3033
+ configPath: displayConfigPath,
2680
3034
  summary: {
2681
3035
  specs: specFiles.length,
2682
3036
  scenarios: scenarioFiles.length,
@@ -2685,7 +3039,7 @@ async function createReportData(root, validation, configResult) {
2685
3039
  ui: uiFiles.length,
2686
3040
  db: dbFiles.length
2687
3041
  },
2688
- counts: resolvedValidation.counts
3042
+ counts: normalizedValidation.counts
2689
3043
  },
2690
3044
  ids: {
2691
3045
  spec: idsByPrefix.SPEC,
@@ -2710,21 +3064,23 @@ async function createReportData(root, validation, configResult) {
2710
3064
  specs: {
2711
3065
  contractRefMissing: specContractRefs.missingRefSpecs.size,
2712
3066
  missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
2713
- specToContractIds: specToContractIdsRecord
3067
+ specToContracts: specToContractsRecord
2714
3068
  }
2715
3069
  },
2716
- issues: resolvedValidation.issues
3070
+ issues: normalizedValidation.issues
2717
3071
  };
2718
3072
  }
2719
3073
  function formatReportMarkdown(data) {
2720
3074
  const lines = [];
2721
3075
  lines.push("# QFAI Report");
3076
+ lines.push("");
2722
3077
  lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
2723
3078
  lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
2724
3079
  lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
2725
3080
  lines.push(`- \u7248: ${data.version}`);
2726
3081
  lines.push("");
2727
3082
  lines.push("## \u6982\u8981");
3083
+ lines.push("");
2728
3084
  lines.push(`- specs: ${data.summary.specs}`);
2729
3085
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2730
3086
  lines.push(
@@ -2735,6 +3091,7 @@ function formatReportMarkdown(data) {
2735
3091
  );
2736
3092
  lines.push("");
2737
3093
  lines.push("## ID\u96C6\u8A08");
3094
+ lines.push("");
2738
3095
  lines.push(formatIdLine("SPEC", data.ids.spec));
2739
3096
  lines.push(formatIdLine("BR", data.ids.br));
2740
3097
  lines.push(formatIdLine("SC", data.ids.sc));
@@ -2743,12 +3100,14 @@ function formatReportMarkdown(data) {
2743
3100
  lines.push(formatIdLine("DB", data.ids.db));
2744
3101
  lines.push("");
2745
3102
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
3103
+ lines.push("");
2746
3104
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
2747
3105
  lines.push(
2748
3106
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2749
3107
  );
2750
3108
  lines.push("");
2751
3109
  lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
3110
+ lines.push("");
2752
3111
  lines.push(`- total: ${data.traceability.contracts.total}`);
2753
3112
  lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
2754
3113
  lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
@@ -2757,6 +3116,7 @@ function formatReportMarkdown(data) {
2757
3116
  );
2758
3117
  lines.push("");
2759
3118
  lines.push("## \u5951\u7D04\u2192Spec");
3119
+ lines.push("");
2760
3120
  const contractToSpecs = data.traceability.contracts.idToSpecs;
2761
3121
  const contractIds = Object.keys(contractToSpecs).sort(
2762
3122
  (a, b) => a.localeCompare(b)
@@ -2775,24 +3135,25 @@ function formatReportMarkdown(data) {
2775
3135
  }
2776
3136
  lines.push("");
2777
3137
  lines.push("## Spec\u2192\u5951\u7D04");
2778
- const specToContracts = data.traceability.specs.specToContractIds;
3138
+ lines.push("");
3139
+ const specToContracts = data.traceability.specs.specToContracts;
2779
3140
  const specIds = Object.keys(specToContracts).sort(
2780
3141
  (a, b) => a.localeCompare(b)
2781
3142
  );
2782
3143
  if (specIds.length === 0) {
2783
3144
  lines.push("- (none)");
2784
3145
  } else {
2785
- for (const specId of specIds) {
2786
- const contractIds2 = specToContracts[specId] ?? [];
2787
- if (contractIds2.length === 0) {
2788
- lines.push(`- ${specId}: (none)`);
2789
- } else {
2790
- lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
2791
- }
2792
- }
3146
+ const rows = specIds.map((specId) => {
3147
+ const entry = specToContracts[specId];
3148
+ const contracts = entry?.status === "missing" ? "(missing)" : entry && entry.ids.length > 0 ? entry.ids.join(", ") : "(none)";
3149
+ const status = entry?.status ?? "missing";
3150
+ return [specId, status, contracts];
3151
+ });
3152
+ lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
2793
3153
  }
2794
3154
  lines.push("");
2795
3155
  lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
3156
+ lines.push("");
2796
3157
  const missingRefSpecs = data.traceability.specs.missingRefSpecs;
2797
3158
  if (missingRefSpecs.length === 0) {
2798
3159
  lines.push("- (none)");
@@ -2803,6 +3164,7 @@ function formatReportMarkdown(data) {
2803
3164
  }
2804
3165
  lines.push("");
2805
3166
  lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
3167
+ lines.push("");
2806
3168
  lines.push(`- total: ${data.traceability.sc.total}`);
2807
3169
  lines.push(`- covered: ${data.traceability.sc.covered}`);
2808
3170
  lines.push(`- missing: ${data.traceability.sc.missing}`);
@@ -2832,6 +3194,7 @@ function formatReportMarkdown(data) {
2832
3194
  }
2833
3195
  lines.push("");
2834
3196
  lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
3197
+ lines.push("");
2835
3198
  const scRefs = data.traceability.sc.refs;
2836
3199
  const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2837
3200
  if (scIds.length === 0) {
@@ -2848,6 +3211,7 @@ function formatReportMarkdown(data) {
2848
3211
  }
2849
3212
  lines.push("");
2850
3213
  lines.push("## Spec:SC=1:1 \u9055\u53CD");
3214
+ lines.push("");
2851
3215
  const specScIssues = data.issues.filter(
2852
3216
  (item) => item.code === "QFAI-TRACE-012"
2853
3217
  );
@@ -2862,6 +3226,7 @@ function formatReportMarkdown(data) {
2862
3226
  }
2863
3227
  lines.push("");
2864
3228
  lines.push("## Hotspots");
3229
+ lines.push("");
2865
3230
  const hotspots = buildHotspots(data.issues);
2866
3231
  if (hotspots.length === 0) {
2867
3232
  lines.push("- (none)");
@@ -2874,6 +3239,7 @@ function formatReportMarkdown(data) {
2874
3239
  }
2875
3240
  lines.push("");
2876
3241
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
3242
+ lines.push("");
2877
3243
  const traceIssues = data.issues.filter(
2878
3244
  (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
2879
3245
  );
@@ -2889,6 +3255,7 @@ function formatReportMarkdown(data) {
2889
3255
  }
2890
3256
  lines.push("");
2891
3257
  lines.push("## \u691C\u8A3C\u7D50\u679C");
3258
+ lines.push("");
2892
3259
  if (data.issues.length === 0) {
2893
3260
  lines.push("- (none)");
2894
3261
  } else {
@@ -2906,33 +3273,40 @@ function formatReportJson(data) {
2906
3273
  return JSON.stringify(data, null, 2);
2907
3274
  }
2908
3275
  async function collectSpecContractRefs(specFiles, contractIdList) {
2909
- const specToContractIds = /* @__PURE__ */ new Map();
3276
+ const specToContracts = /* @__PURE__ */ new Map();
2910
3277
  const idToSpecs = /* @__PURE__ */ new Map();
2911
3278
  const missingRefSpecs = /* @__PURE__ */ new Set();
2912
3279
  for (const contractId of contractIdList) {
2913
3280
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
2914
3281
  }
2915
3282
  for (const file of specFiles) {
2916
- const text = await (0, import_promises15.readFile)(file, "utf-8");
3283
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
2917
3284
  const parsed = parseSpec(text, file);
2918
- const specKey = parsed.specId ?? file;
3285
+ const specKey = parsed.specId;
3286
+ if (!specKey) {
3287
+ continue;
3288
+ }
2919
3289
  const refs = parsed.contractRefs;
2920
3290
  if (refs.lines.length === 0) {
2921
3291
  missingRefSpecs.add(specKey);
3292
+ specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
2922
3293
  continue;
2923
3294
  }
2924
- const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
3295
+ const current = specToContracts.get(specKey) ?? {
3296
+ status: "declared",
3297
+ ids: /* @__PURE__ */ new Set()
3298
+ };
2925
3299
  for (const id of refs.ids) {
2926
- currentContracts.add(id);
3300
+ current.ids.add(id);
2927
3301
  const specs = idToSpecs.get(id);
2928
3302
  if (specs) {
2929
3303
  specs.add(specKey);
2930
3304
  }
2931
3305
  }
2932
- specToContractIds.set(specKey, currentContracts);
3306
+ specToContracts.set(specKey, current);
2933
3307
  }
2934
3308
  return {
2935
- specToContractIds,
3309
+ specToContracts,
2936
3310
  idToSpecs,
2937
3311
  missingRefSpecs
2938
3312
  };
@@ -2947,7 +3321,7 @@ async function collectIds(files) {
2947
3321
  DB: /* @__PURE__ */ new Set()
2948
3322
  };
2949
3323
  for (const file of files) {
2950
- const text = await (0, import_promises15.readFile)(file, "utf-8");
3324
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
2951
3325
  for (const prefix of ID_PREFIXES2) {
2952
3326
  const ids = extractIds(text, prefix);
2953
3327
  ids.forEach((id) => result[prefix].add(id));
@@ -2965,7 +3339,7 @@ async function collectIds(files) {
2965
3339
  async function collectUpstreamIds(files) {
2966
3340
  const ids = /* @__PURE__ */ new Set();
2967
3341
  for (const file of files) {
2968
- const text = await (0, import_promises15.readFile)(file, "utf-8");
3342
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
2969
3343
  extractAllIds(text).forEach((id) => ids.add(id));
2970
3344
  }
2971
3345
  return ids;
@@ -2986,7 +3360,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
2986
3360
  }
2987
3361
  const pattern = buildIdPattern2(Array.from(upstreamIds));
2988
3362
  for (const file of targetFiles) {
2989
- const text = await (0, import_promises15.readFile)(file, "utf-8");
3363
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
2990
3364
  if (pattern.test(text)) {
2991
3365
  return true;
2992
3366
  }
@@ -3009,6 +3383,20 @@ function formatList(values) {
3009
3383
  }
3010
3384
  return values.join(", ");
3011
3385
  }
3386
+ function formatMarkdownTable(headers, rows) {
3387
+ const widths = headers.map((header, index) => {
3388
+ const candidates = rows.map((row) => row[index] ?? "");
3389
+ return Math.max(header.length, ...candidates.map((item) => item.length));
3390
+ });
3391
+ const formatRow = (cells) => {
3392
+ const padded = cells.map(
3393
+ (cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)
3394
+ );
3395
+ return `| ${padded.join(" | ")} |`;
3396
+ };
3397
+ const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
3398
+ return [formatRow(headers), separator, ...rows.map(formatRow)];
3399
+ }
3012
3400
  function toSortedArray2(values) {
3013
3401
  return Array.from(values).sort((a, b) => a.localeCompare(b));
3014
3402
  }
@@ -3019,6 +3407,27 @@ function mapToSortedRecord(values) {
3019
3407
  }
3020
3408
  return record2;
3021
3409
  }
3410
+ function mapToSpecContractRecord(values) {
3411
+ const record2 = {};
3412
+ for (const [key, entry] of values.entries()) {
3413
+ record2[key] = {
3414
+ status: entry.status,
3415
+ ids: toSortedArray2(entry.ids)
3416
+ };
3417
+ }
3418
+ return record2;
3419
+ }
3420
+ function normalizeScSources(root, sources) {
3421
+ const normalized = /* @__PURE__ */ new Map();
3422
+ for (const [id, files] of sources.entries()) {
3423
+ const mapped = /* @__PURE__ */ new Set();
3424
+ for (const file of files) {
3425
+ mapped.add(toRelativePath(root, file));
3426
+ }
3427
+ normalized.set(id, mapped);
3428
+ }
3429
+ return normalized;
3430
+ }
3022
3431
  function buildHotspots(issues) {
3023
3432
  const map = /* @__PURE__ */ new Map();
3024
3433
  for (const issue7 of issues) {
@@ -3043,39 +3452,54 @@ function buildHotspots(issues) {
3043
3452
 
3044
3453
  // src/cli/commands/report.ts
3045
3454
  async function runReport(options) {
3046
- const root = import_node_path15.default.resolve(options.root);
3455
+ const root = import_node_path18.default.resolve(options.root);
3047
3456
  const configResult = await loadConfig(root);
3048
- const input = configResult.config.output.validateJsonPath;
3049
- const inputPath = import_node_path15.default.isAbsolute(input) ? input : import_node_path15.default.resolve(root, input);
3050
3457
  let validation;
3051
- try {
3052
- validation = await readValidationResult(inputPath);
3053
- } catch (err) {
3054
- if (isMissingFileError5(err)) {
3055
- error(
3056
- [
3057
- `qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
3058
- "",
3059
- "\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
3060
- " qfai validate",
3061
- "\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
3062
- "",
3063
- "GitHub Actions \u30C6\u30F3\u30D7\u30EC\u3092\u4F7F\u3063\u3066\u3044\u308B\u5834\u5408\u306F\u3001workflow \u306E validate \u30B8\u30E7\u30D6\u3092\u5148\u306B\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
3064
- ].join("\n")
3065
- );
3066
- process.exitCode = 2;
3067
- return;
3458
+ if (options.runValidate) {
3459
+ if (options.inputPath) {
3460
+ warn("report: --run-validate \u304C\u6307\u5B9A\u3055\u308C\u305F\u305F\u3081 --in \u306F\u7121\u8996\u3057\u307E\u3059\u3002");
3461
+ }
3462
+ const result = await validateProject(root, configResult);
3463
+ const normalized = normalizeValidationResult(root, result);
3464
+ await writeValidationResult(
3465
+ root,
3466
+ configResult.config.output.validateJsonPath,
3467
+ normalized
3468
+ );
3469
+ validation = normalized;
3470
+ } else {
3471
+ const input = options.inputPath ?? configResult.config.output.validateJsonPath;
3472
+ const inputPath = import_node_path18.default.isAbsolute(input) ? input : import_node_path18.default.resolve(root, input);
3473
+ try {
3474
+ validation = await readValidationResult(inputPath);
3475
+ } catch (err) {
3476
+ if (isMissingFileError5(err)) {
3477
+ error(
3478
+ [
3479
+ `qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
3480
+ "",
3481
+ "\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
3482
+ " qfai validate",
3483
+ "\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
3484
+ "",
3485
+ "\u307E\u305F\u306F report \u306B --run-validate \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
3486
+ "GitHub Actions \u30C6\u30F3\u30D7\u30EC\u3092\u4F7F\u3063\u3066\u3044\u308B\u5834\u5408\u306F\u3001workflow \u306E validate \u30B8\u30E7\u30D6\u3092\u5148\u306B\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
3487
+ ].join("\n")
3488
+ );
3489
+ process.exitCode = 2;
3490
+ return;
3491
+ }
3492
+ throw err;
3068
3493
  }
3069
- throw err;
3070
3494
  }
3071
3495
  const data = await createReportData(root, validation, configResult);
3072
3496
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
3073
3497
  const outRoot = resolvePath(root, configResult.config, "outDir");
3074
- const defaultOut = options.format === "json" ? import_node_path15.default.join(outRoot, "report.json") : import_node_path15.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");
3075
3499
  const out = options.outPath ?? defaultOut;
3076
- const outPath = import_node_path15.default.isAbsolute(out) ? out : import_node_path15.default.resolve(root, out);
3077
- await (0, import_promises16.mkdir)(import_node_path15.default.dirname(outPath), { recursive: true });
3078
- 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}
3079
3503
  `, "utf-8");
3080
3504
  info(
3081
3505
  `report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
@@ -3083,7 +3507,7 @@ async function runReport(options) {
3083
3507
  info(`wrote report: ${outPath}`);
3084
3508
  }
3085
3509
  async function readValidationResult(inputPath) {
3086
- const raw = await (0, import_promises16.readFile)(inputPath, "utf-8");
3510
+ const raw = await (0, import_promises18.readFile)(inputPath, "utf-8");
3087
3511
  const parsed = JSON.parse(raw);
3088
3512
  if (!isValidationResult(parsed)) {
3089
3513
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -3138,10 +3562,16 @@ function isMissingFileError5(error2) {
3138
3562
  const record2 = error2;
3139
3563
  return record2.code === "ENOENT";
3140
3564
  }
3565
+ async function writeValidationResult(root, outputPath, result) {
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)}
3569
+ `, "utf-8");
3570
+ }
3141
3571
 
3142
3572
  // src/cli/commands/validate.ts
3143
- var import_promises17 = require("fs/promises");
3144
- var import_node_path16 = __toESM(require("path"), 1);
3573
+ var import_promises19 = require("fs/promises");
3574
+ var import_node_path19 = __toESM(require("path"), 1);
3145
3575
 
3146
3576
  // src/cli/lib/failOn.ts
3147
3577
  function shouldFail(result, failOn) {
@@ -3156,19 +3586,24 @@ function shouldFail(result, failOn) {
3156
3586
 
3157
3587
  // src/cli/commands/validate.ts
3158
3588
  async function runValidate(options) {
3159
- const root = import_node_path16.default.resolve(options.root);
3589
+ const root = import_node_path19.default.resolve(options.root);
3160
3590
  const configResult = await loadConfig(root);
3161
3591
  const result = await validateProject(root, configResult);
3592
+ const normalized = normalizeValidationResult(root, result);
3162
3593
  const format = options.format ?? "text";
3163
3594
  if (format === "text") {
3164
- emitText(result);
3595
+ emitText(normalized);
3165
3596
  }
3166
3597
  if (format === "github") {
3167
- result.issues.forEach(emitGitHub);
3598
+ const jsonPath = resolveJsonPath(
3599
+ root,
3600
+ configResult.config.output.validateJsonPath
3601
+ );
3602
+ emitGitHubOutput(normalized, root, jsonPath);
3168
3603
  }
3169
- await emitJson(result, root, configResult.config.output.validateJsonPath);
3604
+ await emitJson(normalized, root, configResult.config.output.validateJsonPath);
3170
3605
  const failOn = resolveFailOn(options, configResult.config.validation.failOn);
3171
- return shouldFail(result, failOn) ? 1 : 0;
3606
+ return shouldFail(normalized, failOn) ? 1 : 0;
3172
3607
  }
3173
3608
  function resolveFailOn(options, fallback) {
3174
3609
  if (options.failOn) {
@@ -3193,6 +3628,22 @@ function emitText(result) {
3193
3628
  `
3194
3629
  );
3195
3630
  }
3631
+ function emitGitHubOutput(result, root, jsonPath) {
3632
+ const deduped = dedupeIssues(result.issues);
3633
+ const omitted = Math.max(deduped.length - GITHUB_ANNOTATION_LIMIT, 0);
3634
+ const dropped = Math.max(result.issues.length - deduped.length, 0);
3635
+ emitGitHubSummary(result, {
3636
+ total: deduped.length,
3637
+ omitted,
3638
+ dropped,
3639
+ jsonPath,
3640
+ root
3641
+ });
3642
+ const issues = deduped.slice(0, GITHUB_ANNOTATION_LIMIT);
3643
+ for (const issue7 of issues) {
3644
+ emitGitHub(issue7);
3645
+ }
3646
+ }
3196
3647
  function emitGitHub(issue7) {
3197
3648
  const level = issue7.severity === "error" ? "error" : issue7.severity === "warning" ? "warning" : "notice";
3198
3649
  const file = issue7.file ? `file=${issue7.file}` : "";
@@ -3204,22 +3655,75 @@ function emitGitHub(issue7) {
3204
3655
  `
3205
3656
  );
3206
3657
  }
3658
+ function emitGitHubSummary(result, options) {
3659
+ const summary = [
3660
+ "qfai validate summary:",
3661
+ `error=${result.counts.error}`,
3662
+ `warning=${result.counts.warning}`,
3663
+ `info=${result.counts.info}`,
3664
+ `annotations=${Math.min(options.total, GITHUB_ANNOTATION_LIMIT)}/${options.total}`
3665
+ ].join(" ");
3666
+ process.stdout.write(`${summary}
3667
+ `);
3668
+ if (options.dropped > 0 || options.omitted > 0) {
3669
+ const details = [
3670
+ "qfai validate note:",
3671
+ options.dropped > 0 ? `\u91CD\u8907\u9664\u5916=${options.dropped}` : null,
3672
+ options.omitted > 0 ? `\u4E0A\u9650\u7701\u7565=${options.omitted}` : null
3673
+ ].filter(Boolean).join(" ");
3674
+ process.stdout.write(`${details}
3675
+ `);
3676
+ }
3677
+ const relative = toRelativePath(options.root, options.jsonPath);
3678
+ process.stdout.write(
3679
+ `qfai validate note: \u8A73\u7D30\u306F ${relative} \u307E\u305F\u306F --format text \u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\u3002
3680
+ `
3681
+ );
3682
+ }
3683
+ function dedupeIssues(issues) {
3684
+ const seen = /* @__PURE__ */ new Set();
3685
+ const deduped = [];
3686
+ for (const issue7 of issues) {
3687
+ const key = issueKey(issue7);
3688
+ if (seen.has(key)) {
3689
+ continue;
3690
+ }
3691
+ seen.add(key);
3692
+ deduped.push(issue7);
3693
+ }
3694
+ return deduped;
3695
+ }
3696
+ function issueKey(issue7) {
3697
+ const file = issue7.file ?? "";
3698
+ const line = issue7.loc?.line ?? "";
3699
+ const column = issue7.loc?.column ?? "";
3700
+ return [issue7.code, issue7.severity, issue7.message, file, line, column].join(
3701
+ "|"
3702
+ );
3703
+ }
3207
3704
  async function emitJson(result, root, jsonPath) {
3208
- const abs = import_node_path16.default.isAbsolute(jsonPath) ? jsonPath : import_node_path16.default.resolve(root, jsonPath);
3209
- await (0, import_promises17.mkdir)(import_node_path16.default.dirname(abs), { recursive: true });
3210
- await (0, import_promises17.writeFile)(abs, `${JSON.stringify(result, null, 2)}
3705
+ const abs = resolveJsonPath(root, jsonPath);
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)}
3211
3708
  `, "utf-8");
3212
3709
  }
3710
+ function resolveJsonPath(root, jsonPath) {
3711
+ return import_node_path19.default.isAbsolute(jsonPath) ? jsonPath : import_node_path19.default.resolve(root, jsonPath);
3712
+ }
3713
+ var GITHUB_ANNOTATION_LIMIT = 100;
3213
3714
 
3214
3715
  // src/cli/lib/args.ts
3215
3716
  function parseArgs(argv, cwd) {
3216
3717
  const options = {
3217
3718
  root: cwd,
3719
+ rootExplicit: false,
3218
3720
  dir: cwd,
3219
3721
  force: false,
3220
3722
  yes: false,
3221
3723
  dryRun: false,
3222
3724
  reportFormat: "md",
3725
+ reportRunValidate: false,
3726
+ doctorFormat: "text",
3223
3727
  validateFormat: "text",
3224
3728
  strict: false,
3225
3729
  help: false
@@ -3235,6 +3739,7 @@ function parseArgs(argv, cwd) {
3235
3739
  switch (arg) {
3236
3740
  case "--root":
3237
3741
  options.root = args[i + 1] ?? options.root;
3742
+ options.rootExplicit = true;
3238
3743
  i += 1;
3239
3744
  break;
3240
3745
  case "--dir":
@@ -3271,11 +3776,27 @@ function parseArgs(argv, cwd) {
3271
3776
  {
3272
3777
  const next = args[i + 1];
3273
3778
  if (next) {
3274
- options.reportOut = next;
3779
+ if (command === "doctor") {
3780
+ options.doctorOut = next;
3781
+ } else {
3782
+ options.reportOut = next;
3783
+ }
3784
+ }
3785
+ }
3786
+ i += 1;
3787
+ break;
3788
+ case "--in":
3789
+ {
3790
+ const next = args[i + 1];
3791
+ if (next) {
3792
+ options.reportIn = next;
3275
3793
  }
3276
3794
  }
3277
3795
  i += 1;
3278
3796
  break;
3797
+ case "--run-validate":
3798
+ options.reportRunValidate = true;
3799
+ break;
3279
3800
  case "--help":
3280
3801
  case "-h":
3281
3802
  options.help = true;
@@ -3302,6 +3823,12 @@ function applyFormatOption(command, value, options) {
3302
3823
  }
3303
3824
  return;
3304
3825
  }
3826
+ if (command === "doctor") {
3827
+ if (value === "text" || value === "json") {
3828
+ options.doctorFormat = value;
3829
+ }
3830
+ return;
3831
+ }
3305
3832
  if (value === "md" || value === "json") {
3306
3833
  options.reportFormat = value;
3307
3834
  }
@@ -3327,18 +3854,34 @@ async function run(argv, cwd) {
3327
3854
  });
3328
3855
  return;
3329
3856
  case "validate":
3330
- process.exitCode = await runValidate({
3331
- root: options.root,
3332
- strict: options.strict,
3333
- format: options.validateFormat,
3334
- ...options.failOn !== void 0 ? { failOn: options.failOn } : {}
3335
- });
3857
+ {
3858
+ const resolvedRoot = await resolveRoot(options);
3859
+ process.exitCode = await runValidate({
3860
+ root: resolvedRoot,
3861
+ strict: options.strict,
3862
+ format: options.validateFormat,
3863
+ ...options.failOn !== void 0 ? { failOn: options.failOn } : {}
3864
+ });
3865
+ }
3336
3866
  return;
3337
3867
  case "report":
3338
- await runReport({
3868
+ {
3869
+ const resolvedRoot = await resolveRoot(options);
3870
+ await runReport({
3871
+ root: resolvedRoot,
3872
+ format: options.reportFormat,
3873
+ ...options.reportOut !== void 0 ? { outPath: options.reportOut } : {},
3874
+ ...options.reportIn !== void 0 ? { inputPath: options.reportIn } : {},
3875
+ ...options.reportRunValidate ? { runValidate: true } : {}
3876
+ });
3877
+ }
3878
+ return;
3879
+ case "doctor":
3880
+ await runDoctor({
3339
3881
  root: options.root,
3340
- format: options.reportFormat,
3341
- ...options.reportOut !== void 0 ? { outPath: options.reportOut } : {}
3882
+ rootExplicit: options.rootExplicit,
3883
+ format: options.doctorFormat,
3884
+ ...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {}
3342
3885
  });
3343
3886
  return;
3344
3887
  default:
@@ -3354,6 +3897,7 @@ Commands:
3354
3897
  init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
3355
3898
  validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
3356
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
3357
3901
 
3358
3902
  Options:
3359
3903
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
@@ -3363,12 +3907,27 @@ Options:
3363
3907
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
3364
3908
  --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
3365
3909
  --format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
3910
+ --format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
3366
3911
  --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
3367
3912
  --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
3368
- --out <path> report: \u51FA\u529B\u5148
3913
+ --out <path> report/doctor: \u51FA\u529B\u5148
3914
+ --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
3915
+ --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
3369
3916
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A
3370
3917
  `;
3371
3918
  }
3919
+ async function resolveRoot(options) {
3920
+ if (options.rootExplicit) {
3921
+ return options.root;
3922
+ }
3923
+ const search = await findConfigRoot(options.root);
3924
+ if (!search.found) {
3925
+ warn(
3926
+ `qfai: qfai.config.yaml \u304C\u898B\u3064\u304B\u3089\u306A\u3044\u305F\u3081 defaultConfig \u3092\u4F7F\u7528\u3057\u307E\u3059 (root=${search.root})`
3927
+ );
3928
+ }
3929
+ return search.root;
3930
+ }
3372
3931
 
3373
3932
  // src/cli/index.ts
3374
3933
  run(process.argv.slice(2), process.cwd()).catch((err) => {