qfai 0.2.6 → 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.
@@ -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");
@@ -172,7 +172,6 @@ var defaultConfig = {
172
172
  specDir: ".qfai/spec",
173
173
  decisionsDir: ".qfai/spec/decisions",
174
174
  scenariosDir: ".qfai/spec/scenarios",
175
- rulesDir: ".qfai/rules",
176
175
  contractsDir: ".qfai/contracts",
177
176
  uiContractsDir: ".qfai/contracts/ui",
178
177
  apiContractsDir: ".qfai/contracts/api",
@@ -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);
@@ -610,13 +624,15 @@ function isSpecFile(filePath) {
610
624
  }
611
625
 
612
626
  // src/core/ids.ts
613
- var ID_PATTERNS = {
614
- SPEC: /\bSPEC-[A-Z0-9-]+\b/g,
615
- BR: /\bBR-[A-Z0-9-]+\b/g,
616
- SC: /\bSC-[A-Z0-9-]+\b/g,
617
- UI: /\bUI-[A-Z0-9-]+\b/g,
618
- API: /\bAPI-[A-Z0-9-]+\b/g,
619
- 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
620
636
  };
621
637
  var LOOSE_ID_PATTERNS = {
622
638
  SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
@@ -624,16 +640,17 @@ var LOOSE_ID_PATTERNS = {
624
640
  SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
625
641
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
626
642
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
627
- 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
628
645
  };
629
646
  function extractIds(text, prefix) {
630
- const pattern = ID_PATTERNS[prefix];
647
+ const pattern = STRICT_ID_PATTERNS[prefix];
631
648
  const matches = text.match(pattern);
632
649
  return unique(matches ?? []);
633
650
  }
634
651
  function extractAllIds(text) {
635
652
  const all = [];
636
- Object.keys(ID_PATTERNS).forEach((prefix) => {
653
+ ID_PREFIXES.forEach((prefix) => {
637
654
  all.push(...extractIds(text, prefix));
638
655
  });
639
656
  return unique(all);
@@ -654,7 +671,7 @@ function unique(values) {
654
671
  return Array.from(new Set(values));
655
672
  }
656
673
  function isValidId(value, prefix) {
657
- const pattern = ID_PATTERNS[prefix];
674
+ const pattern = STRICT_ID_PATTERNS[prefix];
658
675
  const strict = new RegExp(pattern.source);
659
676
  return strict.test(value);
660
677
  }
@@ -667,8 +684,8 @@ var import_promises4 = require("fs/promises");
667
684
  var import_node_path7 = __toESM(require("path"), 1);
668
685
  var import_node_url2 = require("url");
669
686
  async function resolveToolVersion() {
670
- if ("0.2.6".length > 0) {
671
- return "0.2.6";
687
+ if ("0.2.9".length > 0) {
688
+ return "0.2.9";
672
689
  }
673
690
  try {
674
691
  const packagePath = resolvePackageJsonPath();
@@ -688,8 +705,50 @@ function resolvePackageJsonPath() {
688
705
 
689
706
  // src/core/validators/contracts.ts
690
707
  var import_promises5 = require("fs/promises");
708
+
709
+ // src/core/contracts.ts
691
710
  var import_node_path8 = __toESM(require("path"), 1);
692
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
693
752
  var SQL_DANGEROUS_PATTERNS = [
694
753
  { pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
695
754
  { pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
@@ -738,12 +797,13 @@ async function validateUiContracts(uiRoot) {
738
797
  "SC",
739
798
  "UI",
740
799
  "API",
741
- "DATA"
800
+ "DATA",
801
+ "ADR"
742
802
  ]);
743
803
  if (invalidIds.length > 0) {
744
804
  issues.push(
745
805
  issue(
746
- "QFAI_ID_INVALID_FORMAT",
806
+ "QFAI-ID-002",
747
807
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
748
808
  "error",
749
809
  file,
@@ -752,30 +812,32 @@ async function validateUiContracts(uiRoot) {
752
812
  )
753
813
  );
754
814
  }
815
+ let doc;
755
816
  try {
756
- const doc = (0, import_yaml2.parse)(text);
757
- const id = typeof doc.id === "string" ? doc.id : "";
758
- if (!id.startsWith("UI-")) {
759
- issues.push(
760
- issue(
761
- "QFAI-UI-001",
762
- "UI \u5951\u7D04\u306E id \u306F UI- \u3067\u59CB\u307E\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
763
- "error",
764
- file,
765
- "contracts.ui.id"
766
- )
767
- );
768
- }
817
+ doc = parseStructuredContract(file, text);
769
818
  } catch (error2) {
770
819
  issues.push(
771
820
  issue(
772
- "QFAI-UI-002",
773
- `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)})`,
774
823
  "error",
775
824
  file,
776
825
  "contracts.ui.parse"
777
826
  )
778
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
+ );
779
841
  }
780
842
  }
781
843
  return issues;
@@ -802,12 +864,13 @@ async function validateApiContracts(apiRoot) {
802
864
  "SC",
803
865
  "UI",
804
866
  "API",
805
- "DATA"
867
+ "DATA",
868
+ "ADR"
806
869
  ]);
807
870
  if (invalidIds.length > 0) {
808
871
  issues.push(
809
872
  issue(
810
- "QFAI_ID_INVALID_FORMAT",
873
+ "QFAI-ID-002",
811
874
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
812
875
  "error",
813
876
  file,
@@ -816,29 +879,43 @@ async function validateApiContracts(apiRoot) {
816
879
  )
817
880
  );
818
881
  }
882
+ let doc;
819
883
  try {
820
- const doc = parseStructured(file, text);
821
- if (!doc || !hasOpenApi(doc)) {
822
- issues.push(
823
- issue(
824
- "QFAI-API-001",
825
- "OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
826
- "error",
827
- file,
828
- "contracts.api.openapi"
829
- )
830
- );
831
- }
884
+ doc = parseStructuredContract(file, text);
832
885
  } catch (error2) {
833
886
  issues.push(
834
887
  issue(
835
- "QFAI-API-002",
836
- `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)})`,
837
890
  "error",
838
891
  file,
839
892
  "contracts.api.parse"
840
893
  )
841
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
+ );
842
919
  }
843
920
  }
844
921
  return issues;
@@ -865,12 +942,13 @@ async function validateDataContracts(dataRoot) {
865
942
  "SC",
866
943
  "UI",
867
944
  "API",
868
- "DATA"
945
+ "DATA",
946
+ "ADR"
869
947
  ]);
870
948
  if (invalidIds.length > 0) {
871
949
  issues.push(
872
950
  issue(
873
- "QFAI_ID_INVALID_FORMAT",
951
+ "QFAI-ID-002",
874
952
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
875
953
  "error",
876
954
  file,
@@ -900,13 +978,6 @@ function lintSql(text, file) {
900
978
  }
901
979
  return issues;
902
980
  }
903
- function parseStructured(file, text) {
904
- const ext = import_node_path8.default.extname(file).toLowerCase();
905
- if (ext === ".json") {
906
- return JSON.parse(text);
907
- }
908
- return (0, import_yaml2.parse)(text);
909
- }
910
981
  function hasOpenApi(doc) {
911
982
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
912
983
  }
@@ -917,25 +988,165 @@ function formatError2(error2) {
917
988
  return String(error2);
918
989
  }
919
990
  function issue(code, message, severity, file, rule, refs) {
920
- const issue5 = {
991
+ const issue6 = {
921
992
  code,
922
993
  severity,
923
994
  message
924
995
  };
925
996
  if (file) {
926
- issue5.file = file;
997
+ issue6.file = file;
927
998
  }
928
999
  if (rule) {
929
- issue5.rule = rule;
1000
+ issue6.rule = rule;
930
1001
  }
931
1002
  if (refs && refs.length > 0) {
932
- issue5.refs = refs;
1003
+ issue6.refs = refs;
933
1004
  }
934
- return issue5;
1005
+ return issue6;
935
1006
  }
936
1007
 
937
- // 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
938
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");
939
1150
  var GIVEN_PATTERN = /\bGiven\b/;
940
1151
  var WHEN_PATTERN = /\bWhen\b/;
941
1152
  var THEN_PATTERN = /\bThen\b/;
@@ -946,7 +1157,7 @@ async function validateScenarios(root, config) {
946
1157
  });
947
1158
  if (files.length === 0) {
948
1159
  return [
949
- issue2(
1160
+ issue3(
950
1161
  "QFAI-SC-000",
951
1162
  "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
952
1163
  "info",
@@ -957,7 +1168,7 @@ async function validateScenarios(root, config) {
957
1168
  }
958
1169
  const issues = [];
959
1170
  for (const file of files) {
960
- const text = await (0, import_promises6.readFile)(file, "utf-8");
1171
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
961
1172
  issues.push(...validateScenarioContent(text, file));
962
1173
  }
963
1174
  return issues;
@@ -970,12 +1181,13 @@ function validateScenarioContent(text, file) {
970
1181
  "SC",
971
1182
  "UI",
972
1183
  "API",
973
- "DATA"
1184
+ "DATA",
1185
+ "ADR"
974
1186
  ]);
975
1187
  if (invalidIds.length > 0) {
976
1188
  issues.push(
977
- issue2(
978
- "QFAI_ID_INVALID_FORMAT",
1189
+ issue3(
1190
+ "QFAI-ID-002",
979
1191
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
980
1192
  "error",
981
1193
  file,
@@ -987,7 +1199,7 @@ function validateScenarioContent(text, file) {
987
1199
  const scIds = extractIds(text, "SC");
988
1200
  if (scIds.length === 0) {
989
1201
  issues.push(
990
- issue2(
1202
+ issue3(
991
1203
  "QFAI-SC-001",
992
1204
  "SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
993
1205
  "error",
@@ -999,7 +1211,7 @@ function validateScenarioContent(text, file) {
999
1211
  const specIds = extractIds(text, "SPEC");
1000
1212
  if (specIds.length === 0) {
1001
1213
  issues.push(
1002
- issue2(
1214
+ issue3(
1003
1215
  "QFAI-SC-002",
1004
1216
  "SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
1005
1217
  "error",
@@ -1011,7 +1223,7 @@ function validateScenarioContent(text, file) {
1011
1223
  const brIds = extractIds(text, "BR");
1012
1224
  if (brIds.length === 0) {
1013
1225
  issues.push(
1014
- issue2(
1226
+ issue3(
1015
1227
  "QFAI-SC-003",
1016
1228
  "SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
1017
1229
  "error",
@@ -1032,7 +1244,7 @@ function validateScenarioContent(text, file) {
1032
1244
  }
1033
1245
  if (missingSteps.length > 0) {
1034
1246
  issues.push(
1035
- issue2(
1247
+ issue3(
1036
1248
  "QFAI-SC-005",
1037
1249
  `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1038
1250
  "warning",
@@ -1043,33 +1255,33 @@ function validateScenarioContent(text, file) {
1043
1255
  }
1044
1256
  return issues;
1045
1257
  }
1046
- function issue2(code, message, severity, file, rule, refs) {
1047
- const issue5 = {
1258
+ function issue3(code, message, severity, file, rule, refs) {
1259
+ const issue6 = {
1048
1260
  code,
1049
1261
  severity,
1050
1262
  message
1051
1263
  };
1052
1264
  if (file) {
1053
- issue5.file = file;
1265
+ issue6.file = file;
1054
1266
  }
1055
1267
  if (rule) {
1056
- issue5.rule = rule;
1268
+ issue6.rule = rule;
1057
1269
  }
1058
1270
  if (refs && refs.length > 0) {
1059
- issue5.refs = refs;
1271
+ issue6.refs = refs;
1060
1272
  }
1061
- return issue5;
1273
+ return issue6;
1062
1274
  }
1063
1275
 
1064
1276
  // src/core/validators/spec.ts
1065
- var import_promises7 = require("fs/promises");
1277
+ var import_promises9 = require("fs/promises");
1066
1278
  async function validateSpecs(root, config) {
1067
1279
  const specsRoot = resolvePath(root, config, "specDir");
1068
1280
  const files = await collectSpecFiles(specsRoot);
1069
1281
  if (files.length === 0) {
1070
1282
  const expected = "spec-0001-<slug>.md";
1071
1283
  return [
1072
- issue3(
1284
+ issue4(
1073
1285
  "QFAI-SPEC-000",
1074
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",
@@ -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