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
@@ -131,9 +131,9 @@ function error(message) {
131
131
  async function runInit(options) {
132
132
  const assetsRoot = getInitAssetsDir();
133
133
  const rootAssets = import_node_path3.default.join(assetsRoot, "root");
134
- const qfaiAssets = import_node_path3.default.join(assetsRoot, "qfai");
134
+ const qfaiAssets = import_node_path3.default.join(assetsRoot, ".qfai");
135
135
  const destRoot = import_node_path3.default.resolve(options.dir);
136
- const destQfai = import_node_path3.default.join(destRoot, "qfai");
136
+ const destQfai = import_node_path3.default.join(destRoot, ".qfai");
137
137
  const rootResult = await copyTemplateTree(rootAssets, destRoot, {
138
138
  force: options.force,
139
139
  dryRun: options.dryRun
@@ -160,8 +160,8 @@ function report(copied, skipped, dryRun, label) {
160
160
  }
161
161
 
162
162
  // src/cli/commands/report.ts
163
- var import_promises10 = require("fs/promises");
164
- var import_node_path9 = __toESM(require("path"), 1);
163
+ var import_promises12 = require("fs/promises");
164
+ var import_node_path10 = __toESM(require("path"), 1);
165
165
 
166
166
  // src/core/config.ts
167
167
  var import_promises2 = require("fs/promises");
@@ -169,14 +169,13 @@ var import_node_path4 = __toESM(require("path"), 1);
169
169
  var import_yaml = require("yaml");
170
170
  var defaultConfig = {
171
171
  paths: {
172
- specDir: "qfai/spec",
173
- decisionsDir: "qfai/spec/decisions",
174
- scenariosDir: "qfai/spec",
175
- rulesDir: "qfai/rules",
176
- contractsDir: "qfai/contracts",
177
- uiContractsDir: "qfai/contracts/ui",
178
- apiContractsDir: "qfai/contracts/api",
179
- dataContractsDir: "qfai/contracts/db",
172
+ specDir: ".qfai/spec",
173
+ decisionsDir: ".qfai/spec/decisions",
174
+ scenariosDir: ".qfai/spec/scenarios",
175
+ contractsDir: ".qfai/contracts",
176
+ uiContractsDir: ".qfai/contracts/ui",
177
+ apiContractsDir: ".qfai/contracts/api",
178
+ dataContractsDir: ".qfai/contracts/db",
180
179
  srcDir: "src",
181
180
  testsDir: "tests"
182
181
  },
@@ -196,7 +195,8 @@ var defaultConfig = {
196
195
  traceability: {
197
196
  brMustHaveSc: true,
198
197
  scMustTouchContracts: true,
199
- allowOrphanContracts: false
198
+ allowOrphanContracts: false,
199
+ unknownContractIdSeverity: "error"
200
200
  }
201
201
  },
202
202
  output: {
@@ -271,13 +271,6 @@ function normalizePaths(raw, configPath, issues) {
271
271
  configPath,
272
272
  issues
273
273
  ),
274
- rulesDir: readString(
275
- raw.rulesDir,
276
- base.rulesDir,
277
- "paths.rulesDir",
278
- configPath,
279
- issues
280
- ),
281
274
  contractsDir: readString(
282
275
  raw.contractsDir,
283
276
  base.contractsDir,
@@ -402,6 +395,13 @@ function normalizeValidation(raw, configPath, issues) {
402
395
  "validation.traceability.allowOrphanContracts",
403
396
  configPath,
404
397
  issues
398
+ ),
399
+ unknownContractIdSeverity: readTraceabilitySeverity(
400
+ traceabilityRaw?.unknownContractIdSeverity,
401
+ base.traceability.unknownContractIdSeverity,
402
+ "validation.traceability.unknownContractIdSeverity",
403
+ configPath,
404
+ issues
405
405
  )
406
406
  }
407
407
  };
@@ -481,6 +481,20 @@ function readFailOn(value, fallback, label, configPath, issues) {
481
481
  }
482
482
  return fallback;
483
483
  }
484
+ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
485
+ if (value === "warning" || value === "error") {
486
+ return value;
487
+ }
488
+ if (value !== void 0) {
489
+ issues.push(
490
+ configIssue(
491
+ configPath,
492
+ `${label} \u306F warning|error \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
493
+ )
494
+ );
495
+ }
496
+ return fallback;
497
+ }
484
498
  function readOutputFormat(value, fallback, label, configPath, issues) {
485
499
  if (value === "text" || value === "json" || value === "github") {
486
500
  return value;
@@ -521,7 +535,7 @@ function isRecord(value) {
521
535
  }
522
536
 
523
537
  // src/core/report.ts
524
- var import_promises9 = require("fs/promises");
538
+ var import_promises11 = require("fs/promises");
525
539
 
526
540
  // src/core/discovery.ts
527
541
  var import_node_path6 = __toESM(require("path"), 1);
@@ -582,8 +596,7 @@ async function exists2(target) {
582
596
  }
583
597
 
584
598
  // src/core/discovery.ts
585
- var LEGACY_SPEC_NAME = "spec.md";
586
- var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/i;
599
+ var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/;
587
600
  async function collectSpecFiles(specRoot) {
588
601
  const files = await collectFiles(specRoot, { extensions: [".md"] });
589
602
  return files.filter((file) => isSpecFile(file));
@@ -607,17 +620,19 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
607
620
  }
608
621
  function isSpecFile(filePath) {
609
622
  const name = import_node_path6.default.basename(filePath).toLowerCase();
610
- return name === LEGACY_SPEC_NAME || SPEC_NAMED_PATTERN.test(name);
623
+ return SPEC_NAMED_PATTERN.test(name);
611
624
  }
612
625
 
613
626
  // src/core/ids.ts
614
- var ID_PATTERNS = {
615
- SPEC: /\bSPEC-[A-Z0-9-]+\b/g,
616
- BR: /\bBR-[A-Z0-9-]+\b/g,
617
- SC: /\bSC-[A-Z0-9-]+\b/g,
618
- UI: /\bUI-[A-Z0-9-]+\b/g,
619
- API: /\bAPI-[A-Z0-9-]+\b/g,
620
- DATA: /\bDATA-[A-Z0-9-]+\b/g
627
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
628
+ var STRICT_ID_PATTERNS = {
629
+ SPEC: /\bSPEC-\d{4}\b/g,
630
+ BR: /\bBR-\d{4}\b/g,
631
+ SC: /\bSC-\d{4}\b/g,
632
+ UI: /\bUI-\d{4}\b/g,
633
+ API: /\bAPI-\d{4}\b/g,
634
+ DATA: /\bDATA-\d{4}\b/g,
635
+ ADR: /\bADR-\d{4}\b/g
621
636
  };
622
637
  var LOOSE_ID_PATTERNS = {
623
638
  SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
@@ -625,16 +640,17 @@ var LOOSE_ID_PATTERNS = {
625
640
  SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
626
641
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
627
642
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
628
- DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi
643
+ DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
644
+ ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
629
645
  };
630
646
  function extractIds(text, prefix) {
631
- const pattern = ID_PATTERNS[prefix];
647
+ const pattern = STRICT_ID_PATTERNS[prefix];
632
648
  const matches = text.match(pattern);
633
649
  return unique(matches ?? []);
634
650
  }
635
651
  function extractAllIds(text) {
636
652
  const all = [];
637
- Object.keys(ID_PATTERNS).forEach((prefix) => {
653
+ ID_PREFIXES.forEach((prefix) => {
638
654
  all.push(...extractIds(text, prefix));
639
655
  });
640
656
  return unique(all);
@@ -655,7 +671,7 @@ function unique(values) {
655
671
  return Array.from(new Set(values));
656
672
  }
657
673
  function isValidId(value, prefix) {
658
- const pattern = ID_PATTERNS[prefix];
674
+ const pattern = STRICT_ID_PATTERNS[prefix];
659
675
  const strict = new RegExp(pattern.source);
660
676
  return strict.test(value);
661
677
  }
@@ -668,8 +684,8 @@ var import_promises4 = require("fs/promises");
668
684
  var import_node_path7 = __toESM(require("path"), 1);
669
685
  var import_node_url2 = require("url");
670
686
  async function resolveToolVersion() {
671
- if ("0.2.5".length > 0) {
672
- return "0.2.5";
687
+ if ("0.2.9".length > 0) {
688
+ return "0.2.9";
673
689
  }
674
690
  try {
675
691
  const packagePath = resolvePackageJsonPath();
@@ -689,8 +705,50 @@ function resolvePackageJsonPath() {
689
705
 
690
706
  // src/core/validators/contracts.ts
691
707
  var import_promises5 = require("fs/promises");
708
+
709
+ // src/core/contracts.ts
692
710
  var import_node_path8 = __toESM(require("path"), 1);
693
711
  var import_yaml2 = require("yaml");
712
+ function parseStructuredContract(file, text) {
713
+ const ext = import_node_path8.default.extname(file).toLowerCase();
714
+ if (ext === ".json") {
715
+ return JSON.parse(text);
716
+ }
717
+ return (0, import_yaml2.parse)(text);
718
+ }
719
+ function extractUiContractIds(doc) {
720
+ const id = typeof doc.id === "string" ? doc.id : "";
721
+ return extractIds(id, "UI");
722
+ }
723
+ function extractApiContractIds(doc) {
724
+ const operationIds = /* @__PURE__ */ new Set();
725
+ collectOperationIds(doc, operationIds);
726
+ const ids = /* @__PURE__ */ new Set();
727
+ for (const operationId of operationIds) {
728
+ extractIds(operationId, "API").forEach((id) => ids.add(id));
729
+ }
730
+ return Array.from(ids);
731
+ }
732
+ function collectOperationIds(value, out) {
733
+ if (!value || typeof value !== "object") {
734
+ return;
735
+ }
736
+ if (Array.isArray(value)) {
737
+ for (const item of value) {
738
+ collectOperationIds(item, out);
739
+ }
740
+ return;
741
+ }
742
+ for (const [key, entry] of Object.entries(value)) {
743
+ if (key === "operationId" && typeof entry === "string") {
744
+ out.add(entry);
745
+ continue;
746
+ }
747
+ collectOperationIds(entry, out);
748
+ }
749
+ }
750
+
751
+ // src/core/validators/contracts.ts
694
752
  var SQL_DANGEROUS_PATTERNS = [
695
753
  { pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
696
754
  { pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
@@ -739,12 +797,13 @@ async function validateUiContracts(uiRoot) {
739
797
  "SC",
740
798
  "UI",
741
799
  "API",
742
- "DATA"
800
+ "DATA",
801
+ "ADR"
743
802
  ]);
744
803
  if (invalidIds.length > 0) {
745
804
  issues.push(
746
805
  issue(
747
- "QFAI_ID_INVALID_FORMAT",
806
+ "QFAI-ID-002",
748
807
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
749
808
  "error",
750
809
  file,
@@ -753,30 +812,32 @@ async function validateUiContracts(uiRoot) {
753
812
  )
754
813
  );
755
814
  }
815
+ let doc;
756
816
  try {
757
- const doc = (0, import_yaml2.parse)(text);
758
- const id = typeof doc.id === "string" ? doc.id : "";
759
- if (!id.startsWith("UI-")) {
760
- issues.push(
761
- issue(
762
- "QFAI-UI-001",
763
- "UI \u5951\u7D04\u306E id \u306F UI- \u3067\u59CB\u307E\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
764
- "error",
765
- file,
766
- "contracts.ui.id"
767
- )
768
- );
769
- }
817
+ doc = parseStructuredContract(file, text);
770
818
  } catch (error2) {
771
819
  issues.push(
772
820
  issue(
773
- "QFAI-UI-002",
774
- `UI YAML \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError2(error2)}`,
821
+ "QFAI-CONTRACT-001",
822
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
775
823
  "error",
776
824
  file,
777
825
  "contracts.ui.parse"
778
826
  )
779
827
  );
828
+ continue;
829
+ }
830
+ const uiIds = extractUiContractIds(doc);
831
+ if (uiIds.length === 0) {
832
+ issues.push(
833
+ issue(
834
+ "QFAI-CONTRACT-002",
835
+ `UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
836
+ "error",
837
+ file,
838
+ "contracts.ui.id"
839
+ )
840
+ );
780
841
  }
781
842
  }
782
843
  return issues;
@@ -803,12 +864,13 @@ async function validateApiContracts(apiRoot) {
803
864
  "SC",
804
865
  "UI",
805
866
  "API",
806
- "DATA"
867
+ "DATA",
868
+ "ADR"
807
869
  ]);
808
870
  if (invalidIds.length > 0) {
809
871
  issues.push(
810
872
  issue(
811
- "QFAI_ID_INVALID_FORMAT",
873
+ "QFAI-ID-002",
812
874
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
813
875
  "error",
814
876
  file,
@@ -817,29 +879,43 @@ async function validateApiContracts(apiRoot) {
817
879
  )
818
880
  );
819
881
  }
882
+ let doc;
820
883
  try {
821
- const doc = parseStructured(file, text);
822
- if (!doc || !hasOpenApi(doc)) {
823
- issues.push(
824
- issue(
825
- "QFAI-API-001",
826
- "OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
827
- "error",
828
- file,
829
- "contracts.api.openapi"
830
- )
831
- );
832
- }
884
+ doc = parseStructuredContract(file, text);
833
885
  } catch (error2) {
834
886
  issues.push(
835
887
  issue(
836
- "QFAI-API-002",
837
- `API \u5B9A\u7FA9\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError2(error2)}`,
888
+ "QFAI-CONTRACT-001",
889
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
838
890
  "error",
839
891
  file,
840
892
  "contracts.api.parse"
841
893
  )
842
894
  );
895
+ continue;
896
+ }
897
+ if (!hasOpenApi(doc)) {
898
+ issues.push(
899
+ issue(
900
+ "QFAI-API-001",
901
+ "OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
902
+ "error",
903
+ file,
904
+ "contracts.api.openapi"
905
+ )
906
+ );
907
+ }
908
+ const apiIds = extractApiContractIds(doc);
909
+ if (apiIds.length === 0) {
910
+ issues.push(
911
+ issue(
912
+ "QFAI-CONTRACT-002",
913
+ `API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
914
+ "error",
915
+ file,
916
+ "contracts.api.id"
917
+ )
918
+ );
843
919
  }
844
920
  }
845
921
  return issues;
@@ -866,12 +942,13 @@ async function validateDataContracts(dataRoot) {
866
942
  "SC",
867
943
  "UI",
868
944
  "API",
869
- "DATA"
945
+ "DATA",
946
+ "ADR"
870
947
  ]);
871
948
  if (invalidIds.length > 0) {
872
949
  issues.push(
873
950
  issue(
874
- "QFAI_ID_INVALID_FORMAT",
951
+ "QFAI-ID-002",
875
952
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
876
953
  "error",
877
954
  file,
@@ -901,13 +978,6 @@ function lintSql(text, file) {
901
978
  }
902
979
  return issues;
903
980
  }
904
- function parseStructured(file, text) {
905
- const ext = import_node_path8.default.extname(file).toLowerCase();
906
- if (ext === ".json") {
907
- return JSON.parse(text);
908
- }
909
- return (0, import_yaml2.parse)(text);
910
- }
911
981
  function hasOpenApi(doc) {
912
982
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
913
983
  }
@@ -918,25 +988,165 @@ function formatError2(error2) {
918
988
  return String(error2);
919
989
  }
920
990
  function issue(code, message, severity, file, rule, refs) {
921
- const issue5 = {
991
+ const issue6 = {
922
992
  code,
923
993
  severity,
924
994
  message
925
995
  };
926
996
  if (file) {
927
- issue5.file = file;
997
+ issue6.file = file;
928
998
  }
929
999
  if (rule) {
930
- issue5.rule = rule;
1000
+ issue6.rule = rule;
931
1001
  }
932
1002
  if (refs && refs.length > 0) {
933
- issue5.refs = refs;
1003
+ issue6.refs = refs;
934
1004
  }
935
- return issue5;
1005
+ return issue6;
936
1006
  }
937
1007
 
938
- // src/core/validators/scenario.ts
1008
+ // src/core/validators/ids.ts
1009
+ var import_promises7 = require("fs/promises");
1010
+ var import_node_path9 = __toESM(require("path"), 1);
1011
+
1012
+ // src/core/contractIndex.ts
939
1013
  var import_promises6 = require("fs/promises");
1014
+ async function buildContractIndex(root, config) {
1015
+ const uiRoot = resolvePath(root, config, "uiContractsDir");
1016
+ const apiRoot = resolvePath(root, config, "apiContractsDir");
1017
+ const dataRoot = resolvePath(root, config, "dataContractsDir");
1018
+ const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1019
+ collectUiContractFiles(uiRoot),
1020
+ collectApiContractFiles(apiRoot),
1021
+ collectDataContractFiles(dataRoot)
1022
+ ]);
1023
+ const index = {
1024
+ ids: /* @__PURE__ */ new Set(),
1025
+ idToFiles: /* @__PURE__ */ new Map(),
1026
+ files: { ui: uiFiles, api: apiFiles, data: dataFiles },
1027
+ structuredParseFailedFiles: /* @__PURE__ */ new Set()
1028
+ };
1029
+ await indexUiContracts(uiFiles, index);
1030
+ await indexApiContracts(apiFiles, index);
1031
+ await indexDataContracts(dataFiles, index);
1032
+ return index;
1033
+ }
1034
+ async function indexUiContracts(files, index) {
1035
+ for (const file of files) {
1036
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
1037
+ try {
1038
+ const doc = parseStructuredContract(file, text);
1039
+ extractUiContractIds(doc).forEach((id) => record(index, id, file));
1040
+ } catch {
1041
+ index.structuredParseFailedFiles.add(file);
1042
+ extractIds(text, "UI").forEach((id) => record(index, id, file));
1043
+ }
1044
+ }
1045
+ }
1046
+ async function indexApiContracts(files, index) {
1047
+ for (const file of files) {
1048
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
1049
+ try {
1050
+ const doc = parseStructuredContract(file, text);
1051
+ extractApiContractIds(doc).forEach((id) => record(index, id, file));
1052
+ } catch {
1053
+ index.structuredParseFailedFiles.add(file);
1054
+ extractIds(text, "API").forEach((id) => record(index, id, file));
1055
+ }
1056
+ }
1057
+ }
1058
+ async function indexDataContracts(files, index) {
1059
+ for (const file of files) {
1060
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
1061
+ extractIds(text, "DATA").forEach((id) => record(index, id, file));
1062
+ }
1063
+ }
1064
+ function record(index, id, file) {
1065
+ index.ids.add(id);
1066
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
1067
+ current.add(file);
1068
+ index.idToFiles.set(id, current);
1069
+ }
1070
+
1071
+ // src/core/validators/ids.ts
1072
+ async function validateDefinedIds(root, config) {
1073
+ const issues = [];
1074
+ const specRoot = resolvePath(root, config, "specDir");
1075
+ const scenarioRoot = resolvePath(root, config, "scenariosDir");
1076
+ const specFiles = await collectSpecFiles(specRoot);
1077
+ const scenarioFiles = await collectFiles(scenarioRoot, {
1078
+ extensions: [".feature"]
1079
+ });
1080
+ const defined = /* @__PURE__ */ new Map();
1081
+ await collectSpecDefinitionIds(specFiles, defined);
1082
+ await collectScenarioDefinitionIds(scenarioFiles, defined);
1083
+ const contractIndex = await buildContractIndex(root, config);
1084
+ for (const [id, files] of contractIndex.idToFiles.entries()) {
1085
+ for (const file of files) {
1086
+ recordId(defined, id, file);
1087
+ }
1088
+ }
1089
+ for (const [id, files] of defined.entries()) {
1090
+ if (files.size <= 1) {
1091
+ continue;
1092
+ }
1093
+ const sorted = Array.from(files).sort();
1094
+ issues.push(
1095
+ issue2(
1096
+ "QFAI-ID-001",
1097
+ `ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
1098
+ "error",
1099
+ sorted[0],
1100
+ "id.duplicate"
1101
+ )
1102
+ );
1103
+ }
1104
+ return issues;
1105
+ }
1106
+ async function collectSpecDefinitionIds(files, out) {
1107
+ for (const file of files) {
1108
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
1109
+ extractIds(text, "SPEC").forEach((id) => recordId(out, id, file));
1110
+ extractIds(text, "BR").forEach((id) => recordId(out, id, file));
1111
+ }
1112
+ }
1113
+ async function collectScenarioDefinitionIds(files, out) {
1114
+ for (const file of files) {
1115
+ const text = await (0, import_promises7.readFile)(file, "utf-8");
1116
+ extractIds(text, "SC").forEach((id) => recordId(out, id, file));
1117
+ }
1118
+ }
1119
+ function recordId(out, id, file) {
1120
+ const current = out.get(id) ?? /* @__PURE__ */ new Set();
1121
+ current.add(file);
1122
+ out.set(id, current);
1123
+ }
1124
+ function formatFileList(files, root) {
1125
+ return files.map((file) => {
1126
+ const relative = import_node_path9.default.relative(root, file);
1127
+ return relative.length > 0 ? relative : file;
1128
+ }).join(", ");
1129
+ }
1130
+ function issue2(code, message, severity, file, rule, refs) {
1131
+ const issue6 = {
1132
+ code,
1133
+ severity,
1134
+ message
1135
+ };
1136
+ if (file) {
1137
+ issue6.file = file;
1138
+ }
1139
+ if (rule) {
1140
+ issue6.rule = rule;
1141
+ }
1142
+ if (refs && refs.length > 0) {
1143
+ issue6.refs = refs;
1144
+ }
1145
+ return issue6;
1146
+ }
1147
+
1148
+ // src/core/validators/scenario.ts
1149
+ var import_promises8 = require("fs/promises");
940
1150
  var GIVEN_PATTERN = /\bGiven\b/;
941
1151
  var WHEN_PATTERN = /\bWhen\b/;
942
1152
  var THEN_PATTERN = /\bThen\b/;
@@ -947,7 +1157,7 @@ async function validateScenarios(root, config) {
947
1157
  });
948
1158
  if (files.length === 0) {
949
1159
  return [
950
- issue2(
1160
+ issue3(
951
1161
  "QFAI-SC-000",
952
1162
  "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
953
1163
  "info",
@@ -958,7 +1168,7 @@ async function validateScenarios(root, config) {
958
1168
  }
959
1169
  const issues = [];
960
1170
  for (const file of files) {
961
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1171
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
962
1172
  issues.push(...validateScenarioContent(text, file));
963
1173
  }
964
1174
  return issues;
@@ -971,12 +1181,13 @@ function validateScenarioContent(text, file) {
971
1181
  "SC",
972
1182
  "UI",
973
1183
  "API",
974
- "DATA"
1184
+ "DATA",
1185
+ "ADR"
975
1186
  ]);
976
1187
  if (invalidIds.length > 0) {
977
1188
  issues.push(
978
- issue2(
979
- "QFAI_ID_INVALID_FORMAT",
1189
+ issue3(
1190
+ "QFAI-ID-002",
980
1191
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
981
1192
  "error",
982
1193
  file,
@@ -988,7 +1199,7 @@ function validateScenarioContent(text, file) {
988
1199
  const scIds = extractIds(text, "SC");
989
1200
  if (scIds.length === 0) {
990
1201
  issues.push(
991
- issue2(
1202
+ issue3(
992
1203
  "QFAI-SC-001",
993
1204
  "SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
994
1205
  "error",
@@ -1000,7 +1211,7 @@ function validateScenarioContent(text, file) {
1000
1211
  const specIds = extractIds(text, "SPEC");
1001
1212
  if (specIds.length === 0) {
1002
1213
  issues.push(
1003
- issue2(
1214
+ issue3(
1004
1215
  "QFAI-SC-002",
1005
1216
  "SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
1006
1217
  "error",
@@ -1012,7 +1223,7 @@ function validateScenarioContent(text, file) {
1012
1223
  const brIds = extractIds(text, "BR");
1013
1224
  if (brIds.length === 0) {
1014
1225
  issues.push(
1015
- issue2(
1226
+ issue3(
1016
1227
  "QFAI-SC-003",
1017
1228
  "SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
1018
1229
  "error",
@@ -1033,7 +1244,7 @@ function validateScenarioContent(text, file) {
1033
1244
  }
1034
1245
  if (missingSteps.length > 0) {
1035
1246
  issues.push(
1036
- issue2(
1247
+ issue3(
1037
1248
  "QFAI-SC-005",
1038
1249
  `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1039
1250
  "warning",
@@ -1044,34 +1255,35 @@ function validateScenarioContent(text, file) {
1044
1255
  }
1045
1256
  return issues;
1046
1257
  }
1047
- function issue2(code, message, severity, file, rule, refs) {
1048
- const issue5 = {
1258
+ function issue3(code, message, severity, file, rule, refs) {
1259
+ const issue6 = {
1049
1260
  code,
1050
1261
  severity,
1051
1262
  message
1052
1263
  };
1053
1264
  if (file) {
1054
- issue5.file = file;
1265
+ issue6.file = file;
1055
1266
  }
1056
1267
  if (rule) {
1057
- issue5.rule = rule;
1268
+ issue6.rule = rule;
1058
1269
  }
1059
1270
  if (refs && refs.length > 0) {
1060
- issue5.refs = refs;
1271
+ issue6.refs = refs;
1061
1272
  }
1062
- return issue5;
1273
+ return issue6;
1063
1274
  }
1064
1275
 
1065
1276
  // src/core/validators/spec.ts
1066
- var import_promises7 = require("fs/promises");
1277
+ var import_promises9 = require("fs/promises");
1067
1278
  async function validateSpecs(root, config) {
1068
1279
  const specsRoot = resolvePath(root, config, "specDir");
1069
1280
  const files = await collectSpecFiles(specsRoot);
1070
1281
  if (files.length === 0) {
1282
+ const expected = "spec-0001-<slug>.md";
1071
1283
  return [
1072
- issue3(
1284
+ issue4(
1073
1285
  "QFAI-SPEC-000",
1074
- "Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1286
+ `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}`,
1075
1287
  "info",
1076
1288
  specsRoot,
1077
1289
  "spec.files"
@@ -1080,7 +1292,7 @@ async function validateSpecs(root, config) {
1080
1292
  }
1081
1293
  const issues = [];
1082
1294
  for (const file of files) {
1083
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1295
+ const text = await (0, import_promises9.readFile)(file, "utf-8");
1084
1296
  issues.push(
1085
1297
  ...validateSpecContent(
1086
1298
  text,
@@ -1099,12 +1311,13 @@ function validateSpecContent(text, file, requiredSections) {
1099
1311
  "SC",
1100
1312
  "UI",
1101
1313
  "API",
1102
- "DATA"
1314
+ "DATA",
1315
+ "ADR"
1103
1316
  ]);
1104
1317
  if (invalidIds.length > 0) {
1105
1318
  issues.push(
1106
- issue3(
1107
- "QFAI_ID_INVALID_FORMAT",
1319
+ issue4(
1320
+ "QFAI-ID-002",
1108
1321
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1109
1322
  "error",
1110
1323
  file,
@@ -1116,7 +1329,7 @@ function validateSpecContent(text, file, requiredSections) {
1116
1329
  const specIds = extractIds(text, "SPEC");
1117
1330
  if (specIds.length === 0) {
1118
1331
  issues.push(
1119
- issue3(
1332
+ issue4(
1120
1333
  "QFAI-SPEC-001",
1121
1334
  "SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1122
1335
  "error",
@@ -1128,7 +1341,7 @@ function validateSpecContent(text, file, requiredSections) {
1128
1341
  const brIds = extractIds(text, "BR");
1129
1342
  if (brIds.length === 0) {
1130
1343
  issues.push(
1131
- issue3(
1344
+ issue4(
1132
1345
  "QFAI-SPEC-002",
1133
1346
  "BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1134
1347
  "error",
@@ -1140,7 +1353,7 @@ function validateSpecContent(text, file, requiredSections) {
1140
1353
  const scIds = extractIds(text, "SC");
1141
1354
  if (scIds.length > 0) {
1142
1355
  issues.push(
1143
- issue3(
1356
+ issue4(
1144
1357
  "QFAI-SPEC-003",
1145
1358
  "Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
1146
1359
  "warning",
@@ -1153,7 +1366,7 @@ function validateSpecContent(text, file, requiredSections) {
1153
1366
  for (const section of requiredSections) {
1154
1367
  if (!text.includes(section)) {
1155
1368
  issues.push(
1156
- issue3(
1369
+ issue4(
1157
1370
  "QFAI-SPEC-004",
1158
1371
  `\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
1159
1372
  "error",
@@ -1165,26 +1378,26 @@ function validateSpecContent(text, file, requiredSections) {
1165
1378
  }
1166
1379
  return issues;
1167
1380
  }
1168
- function issue3(code, message, severity, file, rule, refs) {
1169
- const issue5 = {
1381
+ function issue4(code, message, severity, file, rule, refs) {
1382
+ const issue6 = {
1170
1383
  code,
1171
1384
  severity,
1172
1385
  message
1173
1386
  };
1174
1387
  if (file) {
1175
- issue5.file = file;
1388
+ issue6.file = file;
1176
1389
  }
1177
1390
  if (rule) {
1178
- issue5.rule = rule;
1391
+ issue6.rule = rule;
1179
1392
  }
1180
1393
  if (refs && refs.length > 0) {
1181
- issue5.refs = refs;
1394
+ issue6.refs = refs;
1182
1395
  }
1183
- return issue5;
1396
+ return issue6;
1184
1397
  }
1185
1398
 
1186
1399
  // src/core/validators/traceability.ts
1187
- var import_promises8 = require("fs/promises");
1400
+ var import_promises10 = require("fs/promises");
1188
1401
  async function validateTraceability(root, config) {
1189
1402
  const issues = [];
1190
1403
  const specsRoot = resolvePath(root, config, "specDir");
@@ -1200,36 +1413,141 @@ async function validateTraceability(root, config) {
1200
1413
  extensions: [".feature"]
1201
1414
  });
1202
1415
  const upstreamIds = /* @__PURE__ */ new Set();
1416
+ const specIds = /* @__PURE__ */ new Set();
1203
1417
  const brIdsInSpecs = /* @__PURE__ */ new Set();
1204
1418
  const brIdsInScenarios = /* @__PURE__ */ new Set();
1205
1419
  const scIdsInScenarios = /* @__PURE__ */ new Set();
1206
1420
  const scenarioContractIds = /* @__PURE__ */ new Set();
1207
1421
  const scWithContracts = /* @__PURE__ */ new Set();
1208
- for (const file of [...specFiles, ...decisionFiles]) {
1209
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1422
+ const specToBrIds = /* @__PURE__ */ new Map();
1423
+ const contractIndex = await buildContractIndex(root, config);
1424
+ const contractIds = contractIndex.ids;
1425
+ for (const file of specFiles) {
1426
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1427
+ extractAllIds(text).forEach((id) => upstreamIds.add(id));
1428
+ const specIdsInFile = extractIds(text, "SPEC");
1429
+ specIdsInFile.forEach((id) => specIds.add(id));
1430
+ const brIds = extractIds(text, "BR");
1431
+ brIds.forEach((id) => brIdsInSpecs.add(id));
1432
+ const referencedContractIds = /* @__PURE__ */ new Set([
1433
+ ...extractIds(text, "UI"),
1434
+ ...extractIds(text, "API"),
1435
+ ...extractIds(text, "DATA")
1436
+ ]);
1437
+ const unknownContractIds = Array.from(referencedContractIds).filter(
1438
+ (id) => !contractIds.has(id)
1439
+ );
1440
+ if (unknownContractIds.length > 0) {
1441
+ issues.push(
1442
+ issue5(
1443
+ "QFAI-TRACE-009",
1444
+ `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1445
+ ", "
1446
+ )}`,
1447
+ "error",
1448
+ file,
1449
+ "traceability.specContractExists",
1450
+ unknownContractIds
1451
+ )
1452
+ );
1453
+ }
1454
+ for (const specId of specIdsInFile) {
1455
+ const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
1456
+ brIds.forEach((id) => current.add(id));
1457
+ specToBrIds.set(specId, current);
1458
+ }
1459
+ }
1460
+ for (const file of decisionFiles) {
1461
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1210
1462
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1211
- extractIds(text, "BR").forEach((id) => brIdsInSpecs.add(id));
1212
1463
  }
1213
1464
  for (const file of scenarioFiles) {
1214
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1465
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1215
1466
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1467
+ const specIdsInScenario = extractIds(text, "SPEC");
1216
1468
  const brIds = extractIds(text, "BR");
1217
- brIds.forEach((id) => brIdsInScenarios.add(id));
1218
1469
  const scIds = extractIds(text, "SC");
1219
- scIds.forEach((id) => scIdsInScenarios.add(id));
1220
- const contractIds = [
1470
+ const scenarioIds = [
1221
1471
  ...extractIds(text, "UI"),
1222
1472
  ...extractIds(text, "API"),
1223
1473
  ...extractIds(text, "DATA")
1224
1474
  ];
1225
- contractIds.forEach((id) => scenarioContractIds.add(id));
1226
- if (contractIds.length > 0) {
1475
+ brIds.forEach((id) => brIdsInScenarios.add(id));
1476
+ scIds.forEach((id) => scIdsInScenarios.add(id));
1477
+ scenarioIds.forEach((id) => scenarioContractIds.add(id));
1478
+ if (scenarioIds.length > 0) {
1227
1479
  scIds.forEach((id) => scWithContracts.add(id));
1228
1480
  }
1481
+ const unknownSpecIds = specIdsInScenario.filter((id) => !specIds.has(id));
1482
+ if (unknownSpecIds.length > 0) {
1483
+ issues.push(
1484
+ issue5(
1485
+ "QFAI-TRACE-005",
1486
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
1487
+ "error",
1488
+ file,
1489
+ "traceability.scenarioSpecExists",
1490
+ unknownSpecIds
1491
+ )
1492
+ );
1493
+ }
1494
+ const unknownBrIds = brIds.filter((id) => !brIdsInSpecs.has(id));
1495
+ if (unknownBrIds.length > 0) {
1496
+ issues.push(
1497
+ issue5(
1498
+ "QFAI-TRACE-006",
1499
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
1500
+ "error",
1501
+ file,
1502
+ "traceability.scenarioBrExists",
1503
+ unknownBrIds
1504
+ )
1505
+ );
1506
+ }
1507
+ const unknownContractIds = scenarioIds.filter((id) => !contractIds.has(id));
1508
+ if (unknownContractIds.length > 0) {
1509
+ issues.push(
1510
+ issue5(
1511
+ "QFAI-TRACE-008",
1512
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1513
+ ", "
1514
+ )}`,
1515
+ config.validation.traceability.unknownContractIdSeverity,
1516
+ file,
1517
+ "traceability.scenarioContractExists",
1518
+ unknownContractIds
1519
+ )
1520
+ );
1521
+ }
1522
+ if (specIdsInScenario.length > 0) {
1523
+ const allowedBrIds = /* @__PURE__ */ new Set();
1524
+ for (const specId of specIdsInScenario) {
1525
+ const brIdsForSpec = specToBrIds.get(specId);
1526
+ if (!brIdsForSpec) {
1527
+ continue;
1528
+ }
1529
+ brIdsForSpec.forEach((id) => allowedBrIds.add(id));
1530
+ }
1531
+ const invalidBrIds = brIds.filter((id) => !allowedBrIds.has(id));
1532
+ if (invalidBrIds.length > 0) {
1533
+ issues.push(
1534
+ issue5(
1535
+ "QFAI-TRACE-007",
1536
+ `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
1537
+ ", "
1538
+ )} (SPEC: ${specIdsInScenario.join(", ")})`,
1539
+ "error",
1540
+ file,
1541
+ "traceability.scenarioBrUnderSpec",
1542
+ invalidBrIds
1543
+ )
1544
+ );
1545
+ }
1546
+ }
1229
1547
  }
1230
1548
  if (upstreamIds.size === 0) {
1231
1549
  return [
1232
- issue4(
1550
+ issue5(
1233
1551
  "QFAI-TRACE-000",
1234
1552
  "\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1235
1553
  "info",
@@ -1244,7 +1562,7 @@ async function validateTraceability(root, config) {
1244
1562
  );
1245
1563
  if (orphanBrIds.length > 0) {
1246
1564
  issues.push(
1247
- issue4(
1565
+ issue5(
1248
1566
  "QFAI_TRACE_BR_ORPHAN",
1249
1567
  `BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
1250
1568
  "error",
@@ -1261,7 +1579,7 @@ async function validateTraceability(root, config) {
1261
1579
  );
1262
1580
  if (scWithoutContracts.length > 0) {
1263
1581
  issues.push(
1264
- issue4(
1582
+ issue5(
1265
1583
  "QFAI_TRACE_SC_NO_CONTRACT",
1266
1584
  `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
1267
1585
  ", "
@@ -1275,14 +1593,13 @@ async function validateTraceability(root, config) {
1275
1593
  }
1276
1594
  }
1277
1595
  if (!config.validation.traceability.allowOrphanContracts) {
1278
- const contractIds = await collectContractIds(root, config);
1279
1596
  if (contractIds.size > 0) {
1280
1597
  const orphanContracts = Array.from(contractIds).filter(
1281
1598
  (id) => !scenarioContractIds.has(id)
1282
1599
  );
1283
1600
  if (orphanContracts.length > 0) {
1284
1601
  issues.push(
1285
- issue4(
1602
+ issue5(
1286
1603
  "QFAI_CONTRACT_ORPHAN",
1287
1604
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1288
1605
  "error",
@@ -1299,27 +1616,6 @@ async function validateTraceability(root, config) {
1299
1616
  );
1300
1617
  return issues;
1301
1618
  }
1302
- async function collectContractIds(root, config) {
1303
- const contractIds = /* @__PURE__ */ new Set();
1304
- const uiRoot = resolvePath(root, config, "uiContractsDir");
1305
- const apiRoot = resolvePath(root, config, "apiContractsDir");
1306
- const dataRoot = resolvePath(root, config, "dataContractsDir");
1307
- const uiFiles = await collectUiContractFiles(uiRoot);
1308
- const apiFiles = await collectApiContractFiles(apiRoot);
1309
- const dataFiles = await collectDataContractFiles(dataRoot);
1310
- await collectIdsFromFiles(uiFiles, ["UI"], contractIds);
1311
- await collectIdsFromFiles(apiFiles, ["API"], contractIds);
1312
- await collectIdsFromFiles(dataFiles, ["DATA"], contractIds);
1313
- return contractIds;
1314
- }
1315
- async function collectIdsFromFiles(files, prefixes, out) {
1316
- for (const file of files) {
1317
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1318
- for (const prefix of prefixes) {
1319
- extractIds(text, prefix).forEach((id) => out.add(id));
1320
- }
1321
- }
1322
- }
1323
1619
  async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1324
1620
  const issues = [];
1325
1621
  const codeFiles = await collectFiles(srcRoot, {
@@ -1331,7 +1627,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1331
1627
  const targetFiles = [...codeFiles, ...testFiles];
1332
1628
  if (targetFiles.length === 0) {
1333
1629
  issues.push(
1334
- issue4(
1630
+ issue5(
1335
1631
  "QFAI-TRACE-001",
1336
1632
  "\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1337
1633
  "info",
@@ -1344,7 +1640,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1344
1640
  const pattern = buildIdPattern(Array.from(upstreamIds));
1345
1641
  let found = false;
1346
1642
  for (const file of targetFiles) {
1347
- const text = await (0, import_promises8.readFile)(file, "utf-8");
1643
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1348
1644
  if (pattern.test(text)) {
1349
1645
  found = true;
1350
1646
  break;
@@ -1352,7 +1648,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1352
1648
  }
1353
1649
  if (!found) {
1354
1650
  issues.push(
1355
- issue4(
1651
+ issue5(
1356
1652
  "QFAI-TRACE-002",
1357
1653
  "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
1358
1654
  "warning",
@@ -1367,22 +1663,22 @@ function buildIdPattern(ids) {
1367
1663
  const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1368
1664
  return new RegExp(`\\b(${escaped.join("|")})\\b`);
1369
1665
  }
1370
- function issue4(code, message, severity, file, rule, refs) {
1371
- const issue5 = {
1666
+ function issue5(code, message, severity, file, rule, refs) {
1667
+ const issue6 = {
1372
1668
  code,
1373
1669
  severity,
1374
1670
  message
1375
1671
  };
1376
1672
  if (file) {
1377
- issue5.file = file;
1673
+ issue6.file = file;
1378
1674
  }
1379
1675
  if (rule) {
1380
- issue5.rule = rule;
1676
+ issue6.rule = rule;
1381
1677
  }
1382
1678
  if (refs && refs.length > 0) {
1383
- issue5.refs = refs;
1679
+ issue6.refs = refs;
1384
1680
  }
1385
- return issue5;
1681
+ return issue6;
1386
1682
  }
1387
1683
 
1388
1684
  // src/core/validate.ts
@@ -1394,6 +1690,7 @@ async function validateProject(root, configResult) {
1394
1690
  ...await validateSpecs(root, config),
1395
1691
  ...await validateScenarios(root, config),
1396
1692
  ...await validateContracts(root, config),
1693
+ ...await validateDefinedIds(root, config),
1397
1694
  ...await validateTraceability(root, config)
1398
1695
  ];
1399
1696
  const toolVersion = await resolveToolVersion();
@@ -1406,8 +1703,8 @@ async function validateProject(root, configResult) {
1406
1703
  }
1407
1704
  function countIssues(issues) {
1408
1705
  return issues.reduce(
1409
- (acc, issue5) => {
1410
- acc[issue5.severity] += 1;
1706
+ (acc, issue6) => {
1707
+ acc[issue6.severity] += 1;
1411
1708
  return acc;
1412
1709
  },
1413
1710
  { info: 0, warning: 0, error: 0 }
@@ -1415,7 +1712,7 @@ function countIssues(issues) {
1415
1712
  }
1416
1713
 
1417
1714
  // src/core/report.ts
1418
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
1715
+ var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
1419
1716
  async function createReportData(root, validation, configResult) {
1420
1717
  const resolved = configResult ?? await loadConfig(root);
1421
1718
  const config = resolved.config;
@@ -1423,7 +1720,6 @@ async function createReportData(root, validation, configResult) {
1423
1720
  const specRoot = resolvePath(root, config, "specDir");
1424
1721
  const decisionsRoot = resolvePath(root, config, "decisionsDir");
1425
1722
  const scenariosRoot = resolvePath(root, config, "scenariosDir");
1426
- const rulesRoot = resolvePath(root, config, "rulesDir");
1427
1723
  const apiRoot = resolvePath(root, config, "apiContractsDir");
1428
1724
  const uiRoot = resolvePath(root, config, "uiContractsDir");
1429
1725
  const dbRoot = resolvePath(root, config, "dataContractsDir");
@@ -1436,7 +1732,6 @@ async function createReportData(root, validation, configResult) {
1436
1732
  const decisionFiles = await collectFiles(decisionsRoot, {
1437
1733
  extensions: [".md"]
1438
1734
  });
1439
- const ruleFiles = await collectFiles(rulesRoot, { extensions: [".md"] });
1440
1735
  const {
1441
1736
  api: apiFiles,
1442
1737
  ui: uiFiles,
@@ -1446,7 +1741,6 @@ async function createReportData(root, validation, configResult) {
1446
1741
  ...specFiles,
1447
1742
  ...scenarioFiles,
1448
1743
  ...decisionFiles,
1449
- ...ruleFiles,
1450
1744
  ...apiFiles,
1451
1745
  ...uiFiles,
1452
1746
  ...dbFiles
@@ -1472,7 +1766,6 @@ async function createReportData(root, validation, configResult) {
1472
1766
  specs: specFiles.length,
1473
1767
  scenarios: scenarioFiles.length,
1474
1768
  decisions: decisionFiles.length,
1475
- rules: ruleFiles.length,
1476
1769
  contracts: {
1477
1770
  api: apiFiles.length,
1478
1771
  ui: uiFiles.length,
@@ -1507,7 +1800,6 @@ function formatReportMarkdown(data) {
1507
1800
  lines.push(`- specs: ${data.summary.specs}`);
1508
1801
  lines.push(`- scenarios: ${data.summary.scenarios}`);
1509
1802
  lines.push(`- decisions: ${data.summary.decisions}`);
1510
- lines.push(`- rules: ${data.summary.rules}`);
1511
1803
  lines.push(
1512
1804
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
1513
1805
  );
@@ -1543,7 +1835,7 @@ function formatReportMarkdown(data) {
1543
1835
  lines.push("");
1544
1836
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
1545
1837
  const traceIssues = data.issues.filter(
1546
- (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code === "QFAI_CONTRACT_ORPHAN"
1838
+ (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-") || item.code === "QFAI_CONTRACT_ORPHAN"
1547
1839
  );
1548
1840
  if (traceIssues.length === 0) {
1549
1841
  lines.push("- (none)");
@@ -1583,8 +1875,8 @@ async function collectIds(files) {
1583
1875
  DATA: /* @__PURE__ */ new Set()
1584
1876
  };
1585
1877
  for (const file of files) {
1586
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1587
- for (const prefix of ID_PREFIXES) {
1878
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1879
+ for (const prefix of ID_PREFIXES2) {
1588
1880
  const ids = extractIds(text, prefix);
1589
1881
  ids.forEach((id) => result[prefix].add(id));
1590
1882
  }
@@ -1601,7 +1893,7 @@ async function collectIds(files) {
1601
1893
  async function collectUpstreamIds(files) {
1602
1894
  const ids = /* @__PURE__ */ new Set();
1603
1895
  for (const file of files) {
1604
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1896
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1605
1897
  extractAllIds(text).forEach((id) => ids.add(id));
1606
1898
  }
1607
1899
  return ids;
@@ -1622,7 +1914,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
1622
1914
  }
1623
1915
  const pattern = buildIdPattern2(Array.from(upstreamIds));
1624
1916
  for (const file of targetFiles) {
1625
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1917
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1626
1918
  if (pattern.test(text)) {
1627
1919
  return true;
1628
1920
  }
@@ -1644,20 +1936,20 @@ function toSortedArray(values) {
1644
1936
  }
1645
1937
  function buildHotspots(issues) {
1646
1938
  const map = /* @__PURE__ */ new Map();
1647
- for (const issue5 of issues) {
1648
- if (!issue5.file) {
1939
+ for (const issue6 of issues) {
1940
+ if (!issue6.file) {
1649
1941
  continue;
1650
1942
  }
1651
- const current = map.get(issue5.file) ?? {
1652
- file: issue5.file,
1943
+ const current = map.get(issue6.file) ?? {
1944
+ file: issue6.file,
1653
1945
  total: 0,
1654
1946
  error: 0,
1655
1947
  warning: 0,
1656
1948
  info: 0
1657
1949
  };
1658
1950
  current.total += 1;
1659
- current[issue5.severity] += 1;
1660
- map.set(issue5.file, current);
1951
+ current[issue6.severity] += 1;
1952
+ map.set(issue6.file, current);
1661
1953
  }
1662
1954
  return Array.from(map.values()).sort(
1663
1955
  (a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
@@ -1666,10 +1958,10 @@ function buildHotspots(issues) {
1666
1958
 
1667
1959
  // src/cli/commands/report.ts
1668
1960
  async function runReport(options) {
1669
- const root = import_node_path9.default.resolve(options.root);
1961
+ const root = import_node_path10.default.resolve(options.root);
1670
1962
  const configResult = await loadConfig(root);
1671
1963
  const input = options.jsonPath ?? configResult.config.output.jsonPath;
1672
- const inputPath = import_node_path9.default.isAbsolute(input) ? input : import_node_path9.default.resolve(root, input);
1964
+ const inputPath = import_node_path10.default.isAbsolute(input) ? input : import_node_path10.default.resolve(root, input);
1673
1965
  let validation;
1674
1966
  try {
1675
1967
  validation = await readValidationResult(inputPath);
@@ -1694,9 +1986,9 @@ async function runReport(options) {
1694
1986
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
1695
1987
  const defaultOut = options.format === "json" ? ".qfai/out/report.json" : ".qfai/out/report.md";
1696
1988
  const out = options.outPath ?? defaultOut;
1697
- const outPath = import_node_path9.default.isAbsolute(out) ? out : import_node_path9.default.resolve(root, out);
1698
- await (0, import_promises10.mkdir)(import_node_path9.default.dirname(outPath), { recursive: true });
1699
- await (0, import_promises10.writeFile)(outPath, `${output}
1989
+ const outPath = import_node_path10.default.isAbsolute(out) ? out : import_node_path10.default.resolve(root, out);
1990
+ await (0, import_promises12.mkdir)(import_node_path10.default.dirname(outPath), { recursive: true });
1991
+ await (0, import_promises12.writeFile)(outPath, `${output}
1700
1992
  `, "utf-8");
1701
1993
  info(
1702
1994
  `report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
@@ -1704,7 +1996,7 @@ async function runReport(options) {
1704
1996
  info(`wrote report: ${outPath}`);
1705
1997
  }
1706
1998
  async function readValidationResult(inputPath) {
1707
- const raw = await (0, import_promises10.readFile)(inputPath, "utf-8");
1999
+ const raw = await (0, import_promises12.readFile)(inputPath, "utf-8");
1708
2000
  const parsed = JSON.parse(raw);
1709
2001
  if (!isValidationResult(parsed)) {
1710
2002
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -1720,17 +2012,17 @@ function isValidationResult(value) {
1720
2012
  if (!value || typeof value !== "object") {
1721
2013
  return false;
1722
2014
  }
1723
- const record = value;
1724
- if (typeof record.schemaVersion !== "string") {
2015
+ const record2 = value;
2016
+ if (typeof record2.schemaVersion !== "string") {
1725
2017
  return false;
1726
2018
  }
1727
- if (typeof record.toolVersion !== "string") {
2019
+ if (typeof record2.toolVersion !== "string") {
1728
2020
  return false;
1729
2021
  }
1730
- if (!Array.isArray(record.issues)) {
2022
+ if (!Array.isArray(record2.issues)) {
1731
2023
  return false;
1732
2024
  }
1733
- const counts = record.counts;
2025
+ const counts = record2.counts;
1734
2026
  if (!counts) {
1735
2027
  return false;
1736
2028
  }
@@ -1740,13 +2032,13 @@ function isMissingFileError(error2) {
1740
2032
  if (!error2 || typeof error2 !== "object") {
1741
2033
  return false;
1742
2034
  }
1743
- const record = error2;
1744
- return record.code === "ENOENT";
2035
+ const record2 = error2;
2036
+ return record2.code === "ENOENT";
1745
2037
  }
1746
2038
 
1747
2039
  // src/cli/commands/validate.ts
1748
- var import_promises11 = require("fs/promises");
1749
- var import_node_path10 = __toESM(require("path"), 1);
2040
+ var import_promises13 = require("fs/promises");
2041
+ var import_node_path11 = __toESM(require("path"), 1);
1750
2042
 
1751
2043
  // src/cli/lib/failOn.ts
1752
2044
  function shouldFail(result, failOn) {
@@ -1761,7 +2053,7 @@ function shouldFail(result, failOn) {
1761
2053
 
1762
2054
  // src/cli/commands/validate.ts
1763
2055
  async function runValidate(options) {
1764
- const root = import_node_path10.default.resolve(options.root);
2056
+ const root = import_node_path11.default.resolve(options.root);
1765
2057
  const configResult = await loadConfig(root);
1766
2058
  const result = await validateProject(root, configResult);
1767
2059
  const format = options.format ?? configResult.config.output.format;
@@ -1805,21 +2097,21 @@ function emitText(result) {
1805
2097
  `
1806
2098
  );
1807
2099
  }
1808
- function emitGitHub(issue5) {
1809
- const level = issue5.severity === "error" ? "error" : issue5.severity === "warning" ? "warning" : "notice";
1810
- const file = issue5.file ? `file=${issue5.file}` : "";
1811
- const line = issue5.loc?.line ? `,line=${issue5.loc.line}` : "";
1812
- const column = issue5.loc?.column ? `,col=${issue5.loc.column}` : "";
2100
+ function emitGitHub(issue6) {
2101
+ const level = issue6.severity === "error" ? "error" : issue6.severity === "warning" ? "warning" : "notice";
2102
+ const file = issue6.file ? `file=${issue6.file}` : "";
2103
+ const line = issue6.loc?.line ? `,line=${issue6.loc.line}` : "";
2104
+ const column = issue6.loc?.column ? `,col=${issue6.loc.column}` : "";
1813
2105
  const location = file ? ` ${file}${line}${column}` : "";
1814
2106
  process.stdout.write(
1815
- `::${level}${location}::${issue5.code}: ${issue5.message}
2107
+ `::${level}${location}::${issue6.code}: ${issue6.message}
1816
2108
  `
1817
2109
  );
1818
2110
  }
1819
2111
  async function emitJson(result, root, jsonPath) {
1820
- const abs = import_node_path10.default.isAbsolute(jsonPath) ? jsonPath : import_node_path10.default.resolve(root, jsonPath);
1821
- await (0, import_promises11.mkdir)(import_node_path10.default.dirname(abs), { recursive: true });
1822
- await (0, import_promises11.writeFile)(abs, `${JSON.stringify(result, null, 2)}
2112
+ const abs = import_node_path11.default.isAbsolute(jsonPath) ? jsonPath : import_node_path11.default.resolve(root, jsonPath);
2113
+ await (0, import_promises13.mkdir)(import_node_path11.default.dirname(abs), { recursive: true });
2114
+ await (0, import_promises13.writeFile)(abs, `${JSON.stringify(result, null, 2)}
1823
2115
  `, "utf-8");
1824
2116
  }
1825
2117