qfai 0.2.5 → 0.2.8

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.
Files changed (128) hide show
  1. package/README.md +1 -1
  2. package/assets/init/.qfai/README.md +42 -0
  3. package/assets/init/.qfai/contracts/README.md +61 -0
  4. package/assets/init/{qfai → .qfai}/contracts/api/api-0001-sample.yaml +3 -0
  5. package/assets/init/{qfai → .qfai}/contracts/db/db-0001-sample.sql +3 -2
  6. package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +6 -0
  7. package/assets/init/.qfai/out/README.md +17 -0
  8. package/assets/init/.qfai/prompts/README.md +32 -0
  9. package/assets/init/{qfai → .qfai}/prompts/makeBusinessFlow.md +1 -1
  10. package/assets/init/{qfai → .qfai}/prompts/makeOverview.md +1 -1
  11. package/assets/init/.qfai/spec/README.md +80 -0
  12. package/assets/init/.qfai/spec/decisions/ADR-0001.md +9 -0
  13. package/assets/init/.qfai/spec/decisions/README.md +36 -0
  14. package/assets/init/.qfai/spec/scenarios/scenarios.feature +6 -0
  15. package/assets/init/.qfai/spec/spec-0001-sample.md +36 -0
  16. package/assets/init/root/qfai.config.yaml +8 -8
  17. package/dist/cli/index.cjs +498 -206
  18. package/dist/cli/index.cjs.map +1 -1
  19. package/dist/cli/index.d.ts +0 -2
  20. package/dist/cli/index.mjs +495 -203
  21. package/dist/cli/index.mjs.map +1 -1
  22. package/dist/index.cjs +471 -177
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +7 -4
  25. package/dist/index.d.ts +135 -2
  26. package/dist/index.mjs +470 -177
  27. package/dist/index.mjs.map +1 -1
  28. package/package.json +1 -1
  29. package/assets/init/qfai/README.md +0 -6
  30. package/assets/init/qfai/contracts/ui/ui-0001-sample.yaml +0 -4
  31. package/assets/init/qfai/spec/decisions/ADR-0001.md +0 -7
  32. package/assets/init/qfai/spec/scenarios.feature +0 -6
  33. package/assets/init/qfai/spec/spec-0001-sample.md +0 -29
  34. package/dist/cli/commands/init.d.ts +0 -8
  35. package/dist/cli/commands/init.d.ts.map +0 -1
  36. package/dist/cli/commands/init.js +0 -30
  37. package/dist/cli/commands/init.js.map +0 -1
  38. package/dist/cli/commands/report.d.ts +0 -8
  39. package/dist/cli/commands/report.d.ts.map +0 -1
  40. package/dist/cli/commands/report.js +0 -83
  41. package/dist/cli/commands/report.js.map +0 -1
  42. package/dist/cli/commands/validate.d.ts +0 -10
  43. package/dist/cli/commands/validate.d.ts.map +0 -1
  44. package/dist/cli/commands/validate.js +0 -66
  45. package/dist/cli/commands/validate.js.map +0 -1
  46. package/dist/cli/index.d.ts.map +0 -1
  47. package/dist/cli/index.js +0 -7
  48. package/dist/cli/index.js.map +0 -1
  49. package/dist/cli/lib/args.d.ts +0 -19
  50. package/dist/cli/lib/args.d.ts.map +0 -1
  51. package/dist/cli/lib/args.js +0 -107
  52. package/dist/cli/lib/args.js.map +0 -1
  53. package/dist/cli/lib/assets.d.ts +0 -2
  54. package/dist/cli/lib/assets.d.ts.map +0 -1
  55. package/dist/cli/lib/assets.js +0 -8
  56. package/dist/cli/lib/assets.js.map +0 -1
  57. package/dist/cli/lib/failOn.d.ts +0 -5
  58. package/dist/cli/lib/failOn.d.ts.map +0 -1
  59. package/dist/cli/lib/failOn.js +0 -10
  60. package/dist/cli/lib/failOn.js.map +0 -1
  61. package/dist/cli/lib/fs.d.ts +0 -11
  62. package/dist/cli/lib/fs.d.ts.map +0 -1
  63. package/dist/cli/lib/fs.js +0 -91
  64. package/dist/cli/lib/fs.js.map +0 -1
  65. package/dist/cli/lib/logger.d.ts +0 -4
  66. package/dist/cli/lib/logger.d.ts.map +0 -1
  67. package/dist/cli/lib/logger.js +0 -10
  68. package/dist/cli/lib/logger.js.map +0 -1
  69. package/dist/cli/main.d.ts +0 -2
  70. package/dist/cli/main.d.ts.map +0 -1
  71. package/dist/cli/main.js +0 -73
  72. package/dist/cli/main.js.map +0 -1
  73. package/dist/core/config.d.ts +0 -46
  74. package/dist/core/config.d.ts.map +0 -1
  75. package/dist/core/config.js +0 -224
  76. package/dist/core/config.js.map +0 -1
  77. package/dist/core/discovery.d.ts +0 -11
  78. package/dist/core/discovery.d.ts.map +0 -1
  79. package/dist/core/discovery.js +0 -31
  80. package/dist/core/discovery.js.map +0 -1
  81. package/dist/core/fs.d.ts +0 -6
  82. package/dist/core/fs.d.ts.map +0 -1
  83. package/dist/core/fs.js +0 -55
  84. package/dist/core/fs.js.map +0 -1
  85. package/dist/core/ids.d.ts +0 -5
  86. package/dist/core/ids.d.ts.map +0 -1
  87. package/dist/core/ids.js +0 -49
  88. package/dist/core/ids.js.map +0 -1
  89. package/dist/core/index.d.ts +0 -11
  90. package/dist/core/index.d.ts.map +0 -1
  91. package/dist/core/index.js +0 -11
  92. package/dist/core/index.js.map +0 -1
  93. package/dist/core/report.d.ts +0 -41
  94. package/dist/core/report.d.ts.map +0 -1
  95. package/dist/core/report.js +0 -238
  96. package/dist/core/report.js.map +0 -1
  97. package/dist/core/types.d.ts +0 -27
  98. package/dist/core/types.d.ts.map +0 -1
  99. package/dist/core/types.js +0 -2
  100. package/dist/core/types.js.map +0 -1
  101. package/dist/core/validate.d.ts +0 -4
  102. package/dist/core/validate.d.ts.map +0 -1
  103. package/dist/core/validate.js +0 -32
  104. package/dist/core/validate.js.map +0 -1
  105. package/dist/core/validators/contracts.d.ts +0 -5
  106. package/dist/core/validators/contracts.d.ts.map +0 -1
  107. package/dist/core/validators/contracts.js +0 -157
  108. package/dist/core/validators/contracts.js.map +0 -1
  109. package/dist/core/validators/scenario.d.ts +0 -5
  110. package/dist/core/validators/scenario.d.ts.map +0 -1
  111. package/dist/core/validators/scenario.js +0 -82
  112. package/dist/core/validators/scenario.js.map +0 -1
  113. package/dist/core/validators/spec.d.ts +0 -5
  114. package/dist/core/validators/spec.d.ts.map +0 -1
  115. package/dist/core/validators/spec.js +0 -69
  116. package/dist/core/validators/spec.js.map +0 -1
  117. package/dist/core/validators/traceability.d.ts +0 -4
  118. package/dist/core/validators/traceability.d.ts.map +0 -1
  119. package/dist/core/validators/traceability.js +0 -148
  120. package/dist/core/validators/traceability.js.map +0 -1
  121. package/dist/core/version.d.ts +0 -2
  122. package/dist/core/version.d.ts.map +0 -1
  123. package/dist/core/version.js +0 -25
  124. package/dist/core/version.js.map +0 -1
  125. package/dist/index.d.ts.map +0 -1
  126. package/dist/index.js +0 -2
  127. package/dist/index.js.map +0 -1
  128. package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/index.cjs CHANGED
@@ -44,6 +44,7 @@ __export(src_exports, {
44
44
  resolvePath: () => resolvePath,
45
45
  resolveToolVersion: () => resolveToolVersion,
46
46
  validateContracts: () => validateContracts,
47
+ validateDefinedIds: () => validateDefinedIds,
47
48
  validateProject: () => validateProject,
48
49
  validateScenarioContent: () => validateScenarioContent,
49
50
  validateScenarios: () => validateScenarios,
@@ -59,14 +60,13 @@ var import_node_path = __toESM(require("path"), 1);
59
60
  var import_yaml = require("yaml");
60
61
  var defaultConfig = {
61
62
  paths: {
62
- specDir: "qfai/spec",
63
- decisionsDir: "qfai/spec/decisions",
64
- scenariosDir: "qfai/spec",
65
- rulesDir: "qfai/rules",
66
- contractsDir: "qfai/contracts",
67
- uiContractsDir: "qfai/contracts/ui",
68
- apiContractsDir: "qfai/contracts/api",
69
- dataContractsDir: "qfai/contracts/db",
63
+ specDir: ".qfai/spec",
64
+ decisionsDir: ".qfai/spec/decisions",
65
+ scenariosDir: ".qfai/spec/scenarios",
66
+ contractsDir: ".qfai/contracts",
67
+ uiContractsDir: ".qfai/contracts/ui",
68
+ apiContractsDir: ".qfai/contracts/api",
69
+ dataContractsDir: ".qfai/contracts/db",
70
70
  srcDir: "src",
71
71
  testsDir: "tests"
72
72
  },
@@ -86,7 +86,8 @@ var defaultConfig = {
86
86
  traceability: {
87
87
  brMustHaveSc: true,
88
88
  scMustTouchContracts: true,
89
- allowOrphanContracts: false
89
+ allowOrphanContracts: false,
90
+ unknownContractIdSeverity: "error"
90
91
  }
91
92
  },
92
93
  output: {
@@ -161,13 +162,6 @@ function normalizePaths(raw, configPath, issues) {
161
162
  configPath,
162
163
  issues
163
164
  ),
164
- rulesDir: readString(
165
- raw.rulesDir,
166
- base.rulesDir,
167
- "paths.rulesDir",
168
- configPath,
169
- issues
170
- ),
171
165
  contractsDir: readString(
172
166
  raw.contractsDir,
173
167
  base.contractsDir,
@@ -292,6 +286,13 @@ function normalizeValidation(raw, configPath, issues) {
292
286
  "validation.traceability.allowOrphanContracts",
293
287
  configPath,
294
288
  issues
289
+ ),
290
+ unknownContractIdSeverity: readTraceabilitySeverity(
291
+ traceabilityRaw?.unknownContractIdSeverity,
292
+ base.traceability.unknownContractIdSeverity,
293
+ "validation.traceability.unknownContractIdSeverity",
294
+ configPath,
295
+ issues
295
296
  )
296
297
  }
297
298
  };
@@ -371,6 +372,20 @@ function readFailOn(value, fallback, label, configPath, issues) {
371
372
  }
372
373
  return fallback;
373
374
  }
375
+ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
376
+ if (value === "warning" || value === "error") {
377
+ return value;
378
+ }
379
+ if (value !== void 0) {
380
+ issues.push(
381
+ configIssue(
382
+ configPath,
383
+ `${label} \u306F warning|error \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
384
+ )
385
+ );
386
+ }
387
+ return fallback;
388
+ }
374
389
  function readOutputFormat(value, fallback, label, configPath, issues) {
375
390
  if (value === "text" || value === "json" || value === "github") {
376
391
  return value;
@@ -411,13 +426,15 @@ function isRecord(value) {
411
426
  }
412
427
 
413
428
  // src/core/ids.ts
414
- var ID_PATTERNS = {
415
- SPEC: /\bSPEC-[A-Z0-9-]+\b/g,
416
- BR: /\bBR-[A-Z0-9-]+\b/g,
417
- SC: /\bSC-[A-Z0-9-]+\b/g,
418
- UI: /\bUI-[A-Z0-9-]+\b/g,
419
- API: /\bAPI-[A-Z0-9-]+\b/g,
420
- DATA: /\bDATA-[A-Z0-9-]+\b/g
429
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
430
+ var STRICT_ID_PATTERNS = {
431
+ SPEC: /\bSPEC-\d{4}\b/g,
432
+ BR: /\bBR-\d{4}\b/g,
433
+ SC: /\bSC-\d{4}\b/g,
434
+ UI: /\bUI-\d{4}\b/g,
435
+ API: /\bAPI-\d{4}\b/g,
436
+ DATA: /\bDATA-\d{4}\b/g,
437
+ ADR: /\bADR-\d{4}\b/g
421
438
  };
422
439
  var LOOSE_ID_PATTERNS = {
423
440
  SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
@@ -425,16 +442,17 @@ var LOOSE_ID_PATTERNS = {
425
442
  SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
426
443
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
427
444
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
428
- DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi
445
+ DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
446
+ ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
429
447
  };
430
448
  function extractIds(text, prefix) {
431
- const pattern = ID_PATTERNS[prefix];
449
+ const pattern = STRICT_ID_PATTERNS[prefix];
432
450
  const matches = text.match(pattern);
433
451
  return unique(matches ?? []);
434
452
  }
435
453
  function extractAllIds(text) {
436
454
  const all = [];
437
- Object.keys(ID_PATTERNS).forEach((prefix) => {
455
+ ID_PREFIXES.forEach((prefix) => {
438
456
  all.push(...extractIds(text, prefix));
439
457
  });
440
458
  return unique(all);
@@ -455,13 +473,13 @@ function unique(values) {
455
473
  return Array.from(new Set(values));
456
474
  }
457
475
  function isValidId(value, prefix) {
458
- const pattern = ID_PATTERNS[prefix];
476
+ const pattern = STRICT_ID_PATTERNS[prefix];
459
477
  const strict = new RegExp(pattern.source);
460
478
  return strict.test(value);
461
479
  }
462
480
 
463
481
  // src/core/report.ts
464
- var import_promises8 = require("fs/promises");
482
+ var import_promises10 = require("fs/promises");
465
483
 
466
484
  // src/core/discovery.ts
467
485
  var import_node_path3 = __toESM(require("path"), 1);
@@ -522,8 +540,7 @@ async function exists(target) {
522
540
  }
523
541
 
524
542
  // src/core/discovery.ts
525
- var LEGACY_SPEC_NAME = "spec.md";
526
- var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/i;
543
+ var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/;
527
544
  async function collectSpecFiles(specRoot) {
528
545
  const files = await collectFiles(specRoot, { extensions: [".md"] });
529
546
  return files.filter((file) => isSpecFile(file));
@@ -547,7 +564,7 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
547
564
  }
548
565
  function isSpecFile(filePath) {
549
566
  const name = import_node_path3.default.basename(filePath).toLowerCase();
550
- return name === LEGACY_SPEC_NAME || SPEC_NAMED_PATTERN.test(name);
567
+ return SPEC_NAMED_PATTERN.test(name);
551
568
  }
552
569
 
553
570
  // src/core/types.ts
@@ -558,8 +575,8 @@ var import_promises3 = require("fs/promises");
558
575
  var import_node_path4 = __toESM(require("path"), 1);
559
576
  var import_node_url = require("url");
560
577
  async function resolveToolVersion() {
561
- if ("0.2.5".length > 0) {
562
- return "0.2.5";
578
+ if ("0.2.9".length > 0) {
579
+ return "0.2.9";
563
580
  }
564
581
  try {
565
582
  const packagePath = resolvePackageJsonPath();
@@ -579,8 +596,50 @@ function resolvePackageJsonPath() {
579
596
 
580
597
  // src/core/validators/contracts.ts
581
598
  var import_promises4 = require("fs/promises");
599
+
600
+ // src/core/contracts.ts
582
601
  var import_node_path5 = __toESM(require("path"), 1);
583
602
  var import_yaml2 = require("yaml");
603
+ function parseStructuredContract(file, text) {
604
+ const ext = import_node_path5.default.extname(file).toLowerCase();
605
+ if (ext === ".json") {
606
+ return JSON.parse(text);
607
+ }
608
+ return (0, import_yaml2.parse)(text);
609
+ }
610
+ function extractUiContractIds(doc) {
611
+ const id = typeof doc.id === "string" ? doc.id : "";
612
+ return extractIds(id, "UI");
613
+ }
614
+ function extractApiContractIds(doc) {
615
+ const operationIds = /* @__PURE__ */ new Set();
616
+ collectOperationIds(doc, operationIds);
617
+ const ids = /* @__PURE__ */ new Set();
618
+ for (const operationId of operationIds) {
619
+ extractIds(operationId, "API").forEach((id) => ids.add(id));
620
+ }
621
+ return Array.from(ids);
622
+ }
623
+ function collectOperationIds(value, out) {
624
+ if (!value || typeof value !== "object") {
625
+ return;
626
+ }
627
+ if (Array.isArray(value)) {
628
+ for (const item of value) {
629
+ collectOperationIds(item, out);
630
+ }
631
+ return;
632
+ }
633
+ for (const [key, entry] of Object.entries(value)) {
634
+ if (key === "operationId" && typeof entry === "string") {
635
+ out.add(entry);
636
+ continue;
637
+ }
638
+ collectOperationIds(entry, out);
639
+ }
640
+ }
641
+
642
+ // src/core/validators/contracts.ts
584
643
  var SQL_DANGEROUS_PATTERNS = [
585
644
  { pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
586
645
  { pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
@@ -629,12 +688,13 @@ async function validateUiContracts(uiRoot) {
629
688
  "SC",
630
689
  "UI",
631
690
  "API",
632
- "DATA"
691
+ "DATA",
692
+ "ADR"
633
693
  ]);
634
694
  if (invalidIds.length > 0) {
635
695
  issues.push(
636
696
  issue(
637
- "QFAI_ID_INVALID_FORMAT",
697
+ "QFAI-ID-002",
638
698
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
639
699
  "error",
640
700
  file,
@@ -643,30 +703,32 @@ async function validateUiContracts(uiRoot) {
643
703
  )
644
704
  );
645
705
  }
706
+ let doc;
646
707
  try {
647
- const doc = (0, import_yaml2.parse)(text);
648
- const id = typeof doc.id === "string" ? doc.id : "";
649
- if (!id.startsWith("UI-")) {
650
- issues.push(
651
- issue(
652
- "QFAI-UI-001",
653
- "UI \u5951\u7D04\u306E id \u306F UI- \u3067\u59CB\u307E\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
654
- "error",
655
- file,
656
- "contracts.ui.id"
657
- )
658
- );
659
- }
708
+ doc = parseStructuredContract(file, text);
660
709
  } catch (error) {
661
710
  issues.push(
662
711
  issue(
663
- "QFAI-UI-002",
664
- `UI YAML \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError2(error)}`,
712
+ "QFAI-CONTRACT-001",
713
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
665
714
  "error",
666
715
  file,
667
716
  "contracts.ui.parse"
668
717
  )
669
718
  );
719
+ continue;
720
+ }
721
+ const uiIds = extractUiContractIds(doc);
722
+ if (uiIds.length === 0) {
723
+ issues.push(
724
+ issue(
725
+ "QFAI-CONTRACT-002",
726
+ `UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
727
+ "error",
728
+ file,
729
+ "contracts.ui.id"
730
+ )
731
+ );
670
732
  }
671
733
  }
672
734
  return issues;
@@ -693,12 +755,13 @@ async function validateApiContracts(apiRoot) {
693
755
  "SC",
694
756
  "UI",
695
757
  "API",
696
- "DATA"
758
+ "DATA",
759
+ "ADR"
697
760
  ]);
698
761
  if (invalidIds.length > 0) {
699
762
  issues.push(
700
763
  issue(
701
- "QFAI_ID_INVALID_FORMAT",
764
+ "QFAI-ID-002",
702
765
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
703
766
  "error",
704
767
  file,
@@ -707,29 +770,43 @@ async function validateApiContracts(apiRoot) {
707
770
  )
708
771
  );
709
772
  }
773
+ let doc;
710
774
  try {
711
- const doc = parseStructured(file, text);
712
- if (!doc || !hasOpenApi(doc)) {
713
- issues.push(
714
- issue(
715
- "QFAI-API-001",
716
- "OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
717
- "error",
718
- file,
719
- "contracts.api.openapi"
720
- )
721
- );
722
- }
775
+ doc = parseStructuredContract(file, text);
723
776
  } catch (error) {
724
777
  issues.push(
725
778
  issue(
726
- "QFAI-API-002",
727
- `API \u5B9A\u7FA9\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError2(error)}`,
779
+ "QFAI-CONTRACT-001",
780
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
728
781
  "error",
729
782
  file,
730
783
  "contracts.api.parse"
731
784
  )
732
785
  );
786
+ continue;
787
+ }
788
+ if (!hasOpenApi(doc)) {
789
+ issues.push(
790
+ issue(
791
+ "QFAI-API-001",
792
+ "OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
793
+ "error",
794
+ file,
795
+ "contracts.api.openapi"
796
+ )
797
+ );
798
+ }
799
+ const apiIds = extractApiContractIds(doc);
800
+ if (apiIds.length === 0) {
801
+ issues.push(
802
+ issue(
803
+ "QFAI-CONTRACT-002",
804
+ `API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
805
+ "error",
806
+ file,
807
+ "contracts.api.id"
808
+ )
809
+ );
733
810
  }
734
811
  }
735
812
  return issues;
@@ -756,12 +833,13 @@ async function validateDataContracts(dataRoot) {
756
833
  "SC",
757
834
  "UI",
758
835
  "API",
759
- "DATA"
836
+ "DATA",
837
+ "ADR"
760
838
  ]);
761
839
  if (invalidIds.length > 0) {
762
840
  issues.push(
763
841
  issue(
764
- "QFAI_ID_INVALID_FORMAT",
842
+ "QFAI-ID-002",
765
843
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
766
844
  "error",
767
845
  file,
@@ -791,13 +869,6 @@ function lintSql(text, file) {
791
869
  }
792
870
  return issues;
793
871
  }
794
- function parseStructured(file, text) {
795
- const ext = import_node_path5.default.extname(file).toLowerCase();
796
- if (ext === ".json") {
797
- return JSON.parse(text);
798
- }
799
- return (0, import_yaml2.parse)(text);
800
- }
801
872
  function hasOpenApi(doc) {
802
873
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
803
874
  }
@@ -808,25 +879,165 @@ function formatError2(error) {
808
879
  return String(error);
809
880
  }
810
881
  function issue(code, message, severity, file, rule, refs) {
811
- const issue5 = {
882
+ const issue6 = {
812
883
  code,
813
884
  severity,
814
885
  message
815
886
  };
816
887
  if (file) {
817
- issue5.file = file;
888
+ issue6.file = file;
818
889
  }
819
890
  if (rule) {
820
- issue5.rule = rule;
891
+ issue6.rule = rule;
821
892
  }
822
893
  if (refs && refs.length > 0) {
823
- issue5.refs = refs;
894
+ issue6.refs = refs;
824
895
  }
825
- return issue5;
896
+ return issue6;
826
897
  }
827
898
 
828
- // src/core/validators/scenario.ts
899
+ // src/core/validators/ids.ts
900
+ var import_promises6 = require("fs/promises");
901
+ var import_node_path6 = __toESM(require("path"), 1);
902
+
903
+ // src/core/contractIndex.ts
829
904
  var import_promises5 = require("fs/promises");
905
+ async function buildContractIndex(root, config) {
906
+ const uiRoot = resolvePath(root, config, "uiContractsDir");
907
+ const apiRoot = resolvePath(root, config, "apiContractsDir");
908
+ const dataRoot = resolvePath(root, config, "dataContractsDir");
909
+ const [uiFiles, apiFiles, dataFiles] = await Promise.all([
910
+ collectUiContractFiles(uiRoot),
911
+ collectApiContractFiles(apiRoot),
912
+ collectDataContractFiles(dataRoot)
913
+ ]);
914
+ const index = {
915
+ ids: /* @__PURE__ */ new Set(),
916
+ idToFiles: /* @__PURE__ */ new Map(),
917
+ files: { ui: uiFiles, api: apiFiles, data: dataFiles },
918
+ structuredParseFailedFiles: /* @__PURE__ */ new Set()
919
+ };
920
+ await indexUiContracts(uiFiles, index);
921
+ await indexApiContracts(apiFiles, index);
922
+ await indexDataContracts(dataFiles, index);
923
+ return index;
924
+ }
925
+ async function indexUiContracts(files, index) {
926
+ for (const file of files) {
927
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
928
+ try {
929
+ const doc = parseStructuredContract(file, text);
930
+ extractUiContractIds(doc).forEach((id) => record(index, id, file));
931
+ } catch {
932
+ index.structuredParseFailedFiles.add(file);
933
+ extractIds(text, "UI").forEach((id) => record(index, id, file));
934
+ }
935
+ }
936
+ }
937
+ async function indexApiContracts(files, index) {
938
+ for (const file of files) {
939
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
940
+ try {
941
+ const doc = parseStructuredContract(file, text);
942
+ extractApiContractIds(doc).forEach((id) => record(index, id, file));
943
+ } catch {
944
+ index.structuredParseFailedFiles.add(file);
945
+ extractIds(text, "API").forEach((id) => record(index, id, file));
946
+ }
947
+ }
948
+ }
949
+ async function indexDataContracts(files, index) {
950
+ for (const file of files) {
951
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
952
+ extractIds(text, "DATA").forEach((id) => record(index, id, file));
953
+ }
954
+ }
955
+ function record(index, id, file) {
956
+ index.ids.add(id);
957
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
958
+ current.add(file);
959
+ index.idToFiles.set(id, current);
960
+ }
961
+
962
+ // src/core/validators/ids.ts
963
+ async function validateDefinedIds(root, config) {
964
+ const issues = [];
965
+ const specRoot = resolvePath(root, config, "specDir");
966
+ const scenarioRoot = resolvePath(root, config, "scenariosDir");
967
+ const specFiles = await collectSpecFiles(specRoot);
968
+ const scenarioFiles = await collectFiles(scenarioRoot, {
969
+ extensions: [".feature"]
970
+ });
971
+ const defined = /* @__PURE__ */ new Map();
972
+ await collectSpecDefinitionIds(specFiles, defined);
973
+ await collectScenarioDefinitionIds(scenarioFiles, defined);
974
+ const contractIndex = await buildContractIndex(root, config);
975
+ for (const [id, files] of contractIndex.idToFiles.entries()) {
976
+ for (const file of files) {
977
+ recordId(defined, id, file);
978
+ }
979
+ }
980
+ for (const [id, files] of defined.entries()) {
981
+ if (files.size <= 1) {
982
+ continue;
983
+ }
984
+ const sorted = Array.from(files).sort();
985
+ issues.push(
986
+ issue2(
987
+ "QFAI-ID-001",
988
+ `ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
989
+ "error",
990
+ sorted[0],
991
+ "id.duplicate"
992
+ )
993
+ );
994
+ }
995
+ return issues;
996
+ }
997
+ async function collectSpecDefinitionIds(files, out) {
998
+ for (const file of files) {
999
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
1000
+ extractIds(text, "SPEC").forEach((id) => recordId(out, id, file));
1001
+ extractIds(text, "BR").forEach((id) => recordId(out, id, file));
1002
+ }
1003
+ }
1004
+ async function collectScenarioDefinitionIds(files, out) {
1005
+ for (const file of files) {
1006
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
1007
+ extractIds(text, "SC").forEach((id) => recordId(out, id, file));
1008
+ }
1009
+ }
1010
+ function recordId(out, id, file) {
1011
+ const current = out.get(id) ?? /* @__PURE__ */ new Set();
1012
+ current.add(file);
1013
+ out.set(id, current);
1014
+ }
1015
+ function formatFileList(files, root) {
1016
+ return files.map((file) => {
1017
+ const relative = import_node_path6.default.relative(root, file);
1018
+ return relative.length > 0 ? relative : file;
1019
+ }).join(", ");
1020
+ }
1021
+ function issue2(code, message, severity, file, rule, refs) {
1022
+ const issue6 = {
1023
+ code,
1024
+ severity,
1025
+ message
1026
+ };
1027
+ if (file) {
1028
+ issue6.file = file;
1029
+ }
1030
+ if (rule) {
1031
+ issue6.rule = rule;
1032
+ }
1033
+ if (refs && refs.length > 0) {
1034
+ issue6.refs = refs;
1035
+ }
1036
+ return issue6;
1037
+ }
1038
+
1039
+ // src/core/validators/scenario.ts
1040
+ var import_promises7 = require("fs/promises");
830
1041
  var GIVEN_PATTERN = /\bGiven\b/;
831
1042
  var WHEN_PATTERN = /\bWhen\b/;
832
1043
  var THEN_PATTERN = /\bThen\b/;
@@ -837,7 +1048,7 @@ async function validateScenarios(root, config) {
837
1048
  });
838
1049
  if (files.length === 0) {
839
1050
  return [
840
- issue2(
1051
+ issue3(
841
1052
  "QFAI-SC-000",
842
1053
  "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
843
1054
  "info",
@@ -848,7 +1059,7 @@ async function validateScenarios(root, config) {
848
1059
  }
849
1060
  const issues = [];
850
1061
  for (const file of files) {
851
- const text = await (0, import_promises5.readFile)(file, "utf-8");
1062
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
852
1063
  issues.push(...validateScenarioContent(text, file));
853
1064
  }
854
1065
  return issues;
@@ -861,12 +1072,13 @@ function validateScenarioContent(text, file) {
861
1072
  "SC",
862
1073
  "UI",
863
1074
  "API",
864
- "DATA"
1075
+ "DATA",
1076
+ "ADR"
865
1077
  ]);
866
1078
  if (invalidIds.length > 0) {
867
1079
  issues.push(
868
- issue2(
869
- "QFAI_ID_INVALID_FORMAT",
1080
+ issue3(
1081
+ "QFAI-ID-002",
870
1082
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
871
1083
  "error",
872
1084
  file,
@@ -878,7 +1090,7 @@ function validateScenarioContent(text, file) {
878
1090
  const scIds = extractIds(text, "SC");
879
1091
  if (scIds.length === 0) {
880
1092
  issues.push(
881
- issue2(
1093
+ issue3(
882
1094
  "QFAI-SC-001",
883
1095
  "SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
884
1096
  "error",
@@ -890,7 +1102,7 @@ function validateScenarioContent(text, file) {
890
1102
  const specIds = extractIds(text, "SPEC");
891
1103
  if (specIds.length === 0) {
892
1104
  issues.push(
893
- issue2(
1105
+ issue3(
894
1106
  "QFAI-SC-002",
895
1107
  "SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
896
1108
  "error",
@@ -902,7 +1114,7 @@ function validateScenarioContent(text, file) {
902
1114
  const brIds = extractIds(text, "BR");
903
1115
  if (brIds.length === 0) {
904
1116
  issues.push(
905
- issue2(
1117
+ issue3(
906
1118
  "QFAI-SC-003",
907
1119
  "SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
908
1120
  "error",
@@ -923,7 +1135,7 @@ function validateScenarioContent(text, file) {
923
1135
  }
924
1136
  if (missingSteps.length > 0) {
925
1137
  issues.push(
926
- issue2(
1138
+ issue3(
927
1139
  "QFAI-SC-005",
928
1140
  `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
929
1141
  "warning",
@@ -934,34 +1146,35 @@ function validateScenarioContent(text, file) {
934
1146
  }
935
1147
  return issues;
936
1148
  }
937
- function issue2(code, message, severity, file, rule, refs) {
938
- const issue5 = {
1149
+ function issue3(code, message, severity, file, rule, refs) {
1150
+ const issue6 = {
939
1151
  code,
940
1152
  severity,
941
1153
  message
942
1154
  };
943
1155
  if (file) {
944
- issue5.file = file;
1156
+ issue6.file = file;
945
1157
  }
946
1158
  if (rule) {
947
- issue5.rule = rule;
1159
+ issue6.rule = rule;
948
1160
  }
949
1161
  if (refs && refs.length > 0) {
950
- issue5.refs = refs;
1162
+ issue6.refs = refs;
951
1163
  }
952
- return issue5;
1164
+ return issue6;
953
1165
  }
954
1166
 
955
1167
  // src/core/validators/spec.ts
956
- var import_promises6 = require("fs/promises");
1168
+ var import_promises8 = require("fs/promises");
957
1169
  async function validateSpecs(root, config) {
958
1170
  const specsRoot = resolvePath(root, config, "specDir");
959
1171
  const files = await collectSpecFiles(specsRoot);
960
1172
  if (files.length === 0) {
1173
+ const expected = "spec-0001-<slug>.md";
961
1174
  return [
962
- issue3(
1175
+ issue4(
963
1176
  "QFAI-SPEC-000",
964
- "Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1177
+ `Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
965
1178
  "info",
966
1179
  specsRoot,
967
1180
  "spec.files"
@@ -970,7 +1183,7 @@ async function validateSpecs(root, config) {
970
1183
  }
971
1184
  const issues = [];
972
1185
  for (const file of files) {
973
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1186
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
974
1187
  issues.push(
975
1188
  ...validateSpecContent(
976
1189
  text,
@@ -989,12 +1202,13 @@ function validateSpecContent(text, file, requiredSections) {
989
1202
  "SC",
990
1203
  "UI",
991
1204
  "API",
992
- "DATA"
1205
+ "DATA",
1206
+ "ADR"
993
1207
  ]);
994
1208
  if (invalidIds.length > 0) {
995
1209
  issues.push(
996
- issue3(
997
- "QFAI_ID_INVALID_FORMAT",
1210
+ issue4(
1211
+ "QFAI-ID-002",
998
1212
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
999
1213
  "error",
1000
1214
  file,
@@ -1006,7 +1220,7 @@ function validateSpecContent(text, file, requiredSections) {
1006
1220
  const specIds = extractIds(text, "SPEC");
1007
1221
  if (specIds.length === 0) {
1008
1222
  issues.push(
1009
- issue3(
1223
+ issue4(
1010
1224
  "QFAI-SPEC-001",
1011
1225
  "SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1012
1226
  "error",
@@ -1018,7 +1232,7 @@ function validateSpecContent(text, file, requiredSections) {
1018
1232
  const brIds = extractIds(text, "BR");
1019
1233
  if (brIds.length === 0) {
1020
1234
  issues.push(
1021
- issue3(
1235
+ issue4(
1022
1236
  "QFAI-SPEC-002",
1023
1237
  "BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1024
1238
  "error",
@@ -1030,7 +1244,7 @@ function validateSpecContent(text, file, requiredSections) {
1030
1244
  const scIds = extractIds(text, "SC");
1031
1245
  if (scIds.length > 0) {
1032
1246
  issues.push(
1033
- issue3(
1247
+ issue4(
1034
1248
  "QFAI-SPEC-003",
1035
1249
  "Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
1036
1250
  "warning",
@@ -1043,7 +1257,7 @@ function validateSpecContent(text, file, requiredSections) {
1043
1257
  for (const section of requiredSections) {
1044
1258
  if (!text.includes(section)) {
1045
1259
  issues.push(
1046
- issue3(
1260
+ issue4(
1047
1261
  "QFAI-SPEC-004",
1048
1262
  `\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
1049
1263
  "error",
@@ -1055,26 +1269,26 @@ function validateSpecContent(text, file, requiredSections) {
1055
1269
  }
1056
1270
  return issues;
1057
1271
  }
1058
- function issue3(code, message, severity, file, rule, refs) {
1059
- const issue5 = {
1272
+ function issue4(code, message, severity, file, rule, refs) {
1273
+ const issue6 = {
1060
1274
  code,
1061
1275
  severity,
1062
1276
  message
1063
1277
  };
1064
1278
  if (file) {
1065
- issue5.file = file;
1279
+ issue6.file = file;
1066
1280
  }
1067
1281
  if (rule) {
1068
- issue5.rule = rule;
1282
+ issue6.rule = rule;
1069
1283
  }
1070
1284
  if (refs && refs.length > 0) {
1071
- issue5.refs = refs;
1285
+ issue6.refs = refs;
1072
1286
  }
1073
- return issue5;
1287
+ return issue6;
1074
1288
  }
1075
1289
 
1076
1290
  // src/core/validators/traceability.ts
1077
- var import_promises7 = require("fs/promises");
1291
+ var import_promises9 = require("fs/promises");
1078
1292
  async function validateTraceability(root, config) {
1079
1293
  const issues = [];
1080
1294
  const specsRoot = resolvePath(root, config, "specDir");
@@ -1090,36 +1304,141 @@ async function validateTraceability(root, config) {
1090
1304
  extensions: [".feature"]
1091
1305
  });
1092
1306
  const upstreamIds = /* @__PURE__ */ new Set();
1307
+ const specIds = /* @__PURE__ */ new Set();
1093
1308
  const brIdsInSpecs = /* @__PURE__ */ new Set();
1094
1309
  const brIdsInScenarios = /* @__PURE__ */ new Set();
1095
1310
  const scIdsInScenarios = /* @__PURE__ */ new Set();
1096
1311
  const scenarioContractIds = /* @__PURE__ */ new Set();
1097
1312
  const scWithContracts = /* @__PURE__ */ new Set();
1098
- for (const file of [...specFiles, ...decisionFiles]) {
1099
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1313
+ const specToBrIds = /* @__PURE__ */ new Map();
1314
+ const contractIndex = await buildContractIndex(root, config);
1315
+ const contractIds = contractIndex.ids;
1316
+ for (const file of specFiles) {
1317
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1318
+ extractAllIds(text).forEach((id) => upstreamIds.add(id));
1319
+ const specIdsInFile = extractIds(text, "SPEC");
1320
+ specIdsInFile.forEach((id) => specIds.add(id));
1321
+ const brIds = extractIds(text, "BR");
1322
+ brIds.forEach((id) => brIdsInSpecs.add(id));
1323
+ const referencedContractIds = /* @__PURE__ */ new Set([
1324
+ ...extractIds(text, "UI"),
1325
+ ...extractIds(text, "API"),
1326
+ ...extractIds(text, "DATA")
1327
+ ]);
1328
+ const unknownContractIds = Array.from(referencedContractIds).filter(
1329
+ (id) => !contractIds.has(id)
1330
+ );
1331
+ if (unknownContractIds.length > 0) {
1332
+ issues.push(
1333
+ issue5(
1334
+ "QFAI-TRACE-009",
1335
+ `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1336
+ ", "
1337
+ )}`,
1338
+ "error",
1339
+ file,
1340
+ "traceability.specContractExists",
1341
+ unknownContractIds
1342
+ )
1343
+ );
1344
+ }
1345
+ for (const specId of specIdsInFile) {
1346
+ const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
1347
+ brIds.forEach((id) => current.add(id));
1348
+ specToBrIds.set(specId, current);
1349
+ }
1350
+ }
1351
+ for (const file of decisionFiles) {
1352
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1100
1353
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1101
- extractIds(text, "BR").forEach((id) => brIdsInSpecs.add(id));
1102
1354
  }
1103
1355
  for (const file of scenarioFiles) {
1104
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1356
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1105
1357
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1358
+ const specIdsInScenario = extractIds(text, "SPEC");
1106
1359
  const brIds = extractIds(text, "BR");
1107
- brIds.forEach((id) => brIdsInScenarios.add(id));
1108
1360
  const scIds = extractIds(text, "SC");
1109
- scIds.forEach((id) => scIdsInScenarios.add(id));
1110
- const contractIds = [
1361
+ const scenarioIds = [
1111
1362
  ...extractIds(text, "UI"),
1112
1363
  ...extractIds(text, "API"),
1113
1364
  ...extractIds(text, "DATA")
1114
1365
  ];
1115
- contractIds.forEach((id) => scenarioContractIds.add(id));
1116
- if (contractIds.length > 0) {
1366
+ brIds.forEach((id) => brIdsInScenarios.add(id));
1367
+ scIds.forEach((id) => scIdsInScenarios.add(id));
1368
+ scenarioIds.forEach((id) => scenarioContractIds.add(id));
1369
+ if (scenarioIds.length > 0) {
1117
1370
  scIds.forEach((id) => scWithContracts.add(id));
1118
1371
  }
1372
+ const unknownSpecIds = specIdsInScenario.filter((id) => !specIds.has(id));
1373
+ if (unknownSpecIds.length > 0) {
1374
+ issues.push(
1375
+ issue5(
1376
+ "QFAI-TRACE-005",
1377
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
1378
+ "error",
1379
+ file,
1380
+ "traceability.scenarioSpecExists",
1381
+ unknownSpecIds
1382
+ )
1383
+ );
1384
+ }
1385
+ const unknownBrIds = brIds.filter((id) => !brIdsInSpecs.has(id));
1386
+ if (unknownBrIds.length > 0) {
1387
+ issues.push(
1388
+ issue5(
1389
+ "QFAI-TRACE-006",
1390
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
1391
+ "error",
1392
+ file,
1393
+ "traceability.scenarioBrExists",
1394
+ unknownBrIds
1395
+ )
1396
+ );
1397
+ }
1398
+ const unknownContractIds = scenarioIds.filter((id) => !contractIds.has(id));
1399
+ if (unknownContractIds.length > 0) {
1400
+ issues.push(
1401
+ issue5(
1402
+ "QFAI-TRACE-008",
1403
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1404
+ ", "
1405
+ )}`,
1406
+ config.validation.traceability.unknownContractIdSeverity,
1407
+ file,
1408
+ "traceability.scenarioContractExists",
1409
+ unknownContractIds
1410
+ )
1411
+ );
1412
+ }
1413
+ if (specIdsInScenario.length > 0) {
1414
+ const allowedBrIds = /* @__PURE__ */ new Set();
1415
+ for (const specId of specIdsInScenario) {
1416
+ const brIdsForSpec = specToBrIds.get(specId);
1417
+ if (!brIdsForSpec) {
1418
+ continue;
1419
+ }
1420
+ brIdsForSpec.forEach((id) => allowedBrIds.add(id));
1421
+ }
1422
+ const invalidBrIds = brIds.filter((id) => !allowedBrIds.has(id));
1423
+ if (invalidBrIds.length > 0) {
1424
+ issues.push(
1425
+ issue5(
1426
+ "QFAI-TRACE-007",
1427
+ `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
1428
+ ", "
1429
+ )} (SPEC: ${specIdsInScenario.join(", ")})`,
1430
+ "error",
1431
+ file,
1432
+ "traceability.scenarioBrUnderSpec",
1433
+ invalidBrIds
1434
+ )
1435
+ );
1436
+ }
1437
+ }
1119
1438
  }
1120
1439
  if (upstreamIds.size === 0) {
1121
1440
  return [
1122
- issue4(
1441
+ issue5(
1123
1442
  "QFAI-TRACE-000",
1124
1443
  "\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1125
1444
  "info",
@@ -1134,7 +1453,7 @@ async function validateTraceability(root, config) {
1134
1453
  );
1135
1454
  if (orphanBrIds.length > 0) {
1136
1455
  issues.push(
1137
- issue4(
1456
+ issue5(
1138
1457
  "QFAI_TRACE_BR_ORPHAN",
1139
1458
  `BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
1140
1459
  "error",
@@ -1151,7 +1470,7 @@ async function validateTraceability(root, config) {
1151
1470
  );
1152
1471
  if (scWithoutContracts.length > 0) {
1153
1472
  issues.push(
1154
- issue4(
1473
+ issue5(
1155
1474
  "QFAI_TRACE_SC_NO_CONTRACT",
1156
1475
  `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
1157
1476
  ", "
@@ -1165,14 +1484,13 @@ async function validateTraceability(root, config) {
1165
1484
  }
1166
1485
  }
1167
1486
  if (!config.validation.traceability.allowOrphanContracts) {
1168
- const contractIds = await collectContractIds(root, config);
1169
1487
  if (contractIds.size > 0) {
1170
1488
  const orphanContracts = Array.from(contractIds).filter(
1171
1489
  (id) => !scenarioContractIds.has(id)
1172
1490
  );
1173
1491
  if (orphanContracts.length > 0) {
1174
1492
  issues.push(
1175
- issue4(
1493
+ issue5(
1176
1494
  "QFAI_CONTRACT_ORPHAN",
1177
1495
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1178
1496
  "error",
@@ -1189,27 +1507,6 @@ async function validateTraceability(root, config) {
1189
1507
  );
1190
1508
  return issues;
1191
1509
  }
1192
- async function collectContractIds(root, config) {
1193
- const contractIds = /* @__PURE__ */ new Set();
1194
- const uiRoot = resolvePath(root, config, "uiContractsDir");
1195
- const apiRoot = resolvePath(root, config, "apiContractsDir");
1196
- const dataRoot = resolvePath(root, config, "dataContractsDir");
1197
- const uiFiles = await collectUiContractFiles(uiRoot);
1198
- const apiFiles = await collectApiContractFiles(apiRoot);
1199
- const dataFiles = await collectDataContractFiles(dataRoot);
1200
- await collectIdsFromFiles(uiFiles, ["UI"], contractIds);
1201
- await collectIdsFromFiles(apiFiles, ["API"], contractIds);
1202
- await collectIdsFromFiles(dataFiles, ["DATA"], contractIds);
1203
- return contractIds;
1204
- }
1205
- async function collectIdsFromFiles(files, prefixes, out) {
1206
- for (const file of files) {
1207
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1208
- for (const prefix of prefixes) {
1209
- extractIds(text, prefix).forEach((id) => out.add(id));
1210
- }
1211
- }
1212
- }
1213
1510
  async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1214
1511
  const issues = [];
1215
1512
  const codeFiles = await collectFiles(srcRoot, {
@@ -1221,7 +1518,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1221
1518
  const targetFiles = [...codeFiles, ...testFiles];
1222
1519
  if (targetFiles.length === 0) {
1223
1520
  issues.push(
1224
- issue4(
1521
+ issue5(
1225
1522
  "QFAI-TRACE-001",
1226
1523
  "\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1227
1524
  "info",
@@ -1234,7 +1531,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1234
1531
  const pattern = buildIdPattern(Array.from(upstreamIds));
1235
1532
  let found = false;
1236
1533
  for (const file of targetFiles) {
1237
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1534
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1238
1535
  if (pattern.test(text)) {
1239
1536
  found = true;
1240
1537
  break;
@@ -1242,7 +1539,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1242
1539
  }
1243
1540
  if (!found) {
1244
1541
  issues.push(
1245
- issue4(
1542
+ issue5(
1246
1543
  "QFAI-TRACE-002",
1247
1544
  "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
1248
1545
  "warning",
@@ -1257,22 +1554,22 @@ function buildIdPattern(ids) {
1257
1554
  const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1258
1555
  return new RegExp(`\\b(${escaped.join("|")})\\b`);
1259
1556
  }
1260
- function issue4(code, message, severity, file, rule, refs) {
1261
- const issue5 = {
1557
+ function issue5(code, message, severity, file, rule, refs) {
1558
+ const issue6 = {
1262
1559
  code,
1263
1560
  severity,
1264
1561
  message
1265
1562
  };
1266
1563
  if (file) {
1267
- issue5.file = file;
1564
+ issue6.file = file;
1268
1565
  }
1269
1566
  if (rule) {
1270
- issue5.rule = rule;
1567
+ issue6.rule = rule;
1271
1568
  }
1272
1569
  if (refs && refs.length > 0) {
1273
- issue5.refs = refs;
1570
+ issue6.refs = refs;
1274
1571
  }
1275
- return issue5;
1572
+ return issue6;
1276
1573
  }
1277
1574
 
1278
1575
  // src/core/validate.ts
@@ -1284,6 +1581,7 @@ async function validateProject(root, configResult) {
1284
1581
  ...await validateSpecs(root, config),
1285
1582
  ...await validateScenarios(root, config),
1286
1583
  ...await validateContracts(root, config),
1584
+ ...await validateDefinedIds(root, config),
1287
1585
  ...await validateTraceability(root, config)
1288
1586
  ];
1289
1587
  const toolVersion = await resolveToolVersion();
@@ -1296,8 +1594,8 @@ async function validateProject(root, configResult) {
1296
1594
  }
1297
1595
  function countIssues(issues) {
1298
1596
  return issues.reduce(
1299
- (acc, issue5) => {
1300
- acc[issue5.severity] += 1;
1597
+ (acc, issue6) => {
1598
+ acc[issue6.severity] += 1;
1301
1599
  return acc;
1302
1600
  },
1303
1601
  { info: 0, warning: 0, error: 0 }
@@ -1305,7 +1603,7 @@ function countIssues(issues) {
1305
1603
  }
1306
1604
 
1307
1605
  // src/core/report.ts
1308
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
1606
+ var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
1309
1607
  async function createReportData(root, validation, configResult) {
1310
1608
  const resolved = configResult ?? await loadConfig(root);
1311
1609
  const config = resolved.config;
@@ -1313,7 +1611,6 @@ async function createReportData(root, validation, configResult) {
1313
1611
  const specRoot = resolvePath(root, config, "specDir");
1314
1612
  const decisionsRoot = resolvePath(root, config, "decisionsDir");
1315
1613
  const scenariosRoot = resolvePath(root, config, "scenariosDir");
1316
- const rulesRoot = resolvePath(root, config, "rulesDir");
1317
1614
  const apiRoot = resolvePath(root, config, "apiContractsDir");
1318
1615
  const uiRoot = resolvePath(root, config, "uiContractsDir");
1319
1616
  const dbRoot = resolvePath(root, config, "dataContractsDir");
@@ -1326,7 +1623,6 @@ async function createReportData(root, validation, configResult) {
1326
1623
  const decisionFiles = await collectFiles(decisionsRoot, {
1327
1624
  extensions: [".md"]
1328
1625
  });
1329
- const ruleFiles = await collectFiles(rulesRoot, { extensions: [".md"] });
1330
1626
  const {
1331
1627
  api: apiFiles,
1332
1628
  ui: uiFiles,
@@ -1336,7 +1632,6 @@ async function createReportData(root, validation, configResult) {
1336
1632
  ...specFiles,
1337
1633
  ...scenarioFiles,
1338
1634
  ...decisionFiles,
1339
- ...ruleFiles,
1340
1635
  ...apiFiles,
1341
1636
  ...uiFiles,
1342
1637
  ...dbFiles
@@ -1362,7 +1657,6 @@ async function createReportData(root, validation, configResult) {
1362
1657
  specs: specFiles.length,
1363
1658
  scenarios: scenarioFiles.length,
1364
1659
  decisions: decisionFiles.length,
1365
- rules: ruleFiles.length,
1366
1660
  contracts: {
1367
1661
  api: apiFiles.length,
1368
1662
  ui: uiFiles.length,
@@ -1397,7 +1691,6 @@ function formatReportMarkdown(data) {
1397
1691
  lines.push(`- specs: ${data.summary.specs}`);
1398
1692
  lines.push(`- scenarios: ${data.summary.scenarios}`);
1399
1693
  lines.push(`- decisions: ${data.summary.decisions}`);
1400
- lines.push(`- rules: ${data.summary.rules}`);
1401
1694
  lines.push(
1402
1695
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
1403
1696
  );
@@ -1433,7 +1726,7 @@ function formatReportMarkdown(data) {
1433
1726
  lines.push("");
1434
1727
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
1435
1728
  const traceIssues = data.issues.filter(
1436
- (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code === "QFAI_CONTRACT_ORPHAN"
1729
+ (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-") || item.code === "QFAI_CONTRACT_ORPHAN"
1437
1730
  );
1438
1731
  if (traceIssues.length === 0) {
1439
1732
  lines.push("- (none)");
@@ -1473,8 +1766,8 @@ async function collectIds(files) {
1473
1766
  DATA: /* @__PURE__ */ new Set()
1474
1767
  };
1475
1768
  for (const file of files) {
1476
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1477
- for (const prefix of ID_PREFIXES) {
1769
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1770
+ for (const prefix of ID_PREFIXES2) {
1478
1771
  const ids = extractIds(text, prefix);
1479
1772
  ids.forEach((id) => result[prefix].add(id));
1480
1773
  }
@@ -1491,7 +1784,7 @@ async function collectIds(files) {
1491
1784
  async function collectUpstreamIds(files) {
1492
1785
  const ids = /* @__PURE__ */ new Set();
1493
1786
  for (const file of files) {
1494
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1787
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1495
1788
  extractAllIds(text).forEach((id) => ids.add(id));
1496
1789
  }
1497
1790
  return ids;
@@ -1512,7 +1805,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
1512
1805
  }
1513
1806
  const pattern = buildIdPattern2(Array.from(upstreamIds));
1514
1807
  for (const file of targetFiles) {
1515
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1808
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1516
1809
  if (pattern.test(text)) {
1517
1810
  return true;
1518
1811
  }
@@ -1534,20 +1827,20 @@ function toSortedArray(values) {
1534
1827
  }
1535
1828
  function buildHotspots(issues) {
1536
1829
  const map = /* @__PURE__ */ new Map();
1537
- for (const issue5 of issues) {
1538
- if (!issue5.file) {
1830
+ for (const issue6 of issues) {
1831
+ if (!issue6.file) {
1539
1832
  continue;
1540
1833
  }
1541
- const current = map.get(issue5.file) ?? {
1542
- file: issue5.file,
1834
+ const current = map.get(issue6.file) ?? {
1835
+ file: issue6.file,
1543
1836
  total: 0,
1544
1837
  error: 0,
1545
1838
  warning: 0,
1546
1839
  info: 0
1547
1840
  };
1548
1841
  current.total += 1;
1549
- current[issue5.severity] += 1;
1550
- map.set(issue5.file, current);
1842
+ current[issue6.severity] += 1;
1843
+ map.set(issue6.file, current);
1551
1844
  }
1552
1845
  return Array.from(map.values()).sort(
1553
1846
  (a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
@@ -1569,6 +1862,7 @@ function buildHotspots(issues) {
1569
1862
  resolvePath,
1570
1863
  resolveToolVersion,
1571
1864
  validateContracts,
1865
+ validateDefinedIds,
1572
1866
  validateProject,
1573
1867
  validateScenarioContent,
1574
1868
  validateScenarios,