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.
package/dist/index.mjs CHANGED
@@ -7,7 +7,6 @@ var defaultConfig = {
7
7
  specDir: ".qfai/spec",
8
8
  decisionsDir: ".qfai/spec/decisions",
9
9
  scenariosDir: ".qfai/spec/scenarios",
10
- rulesDir: ".qfai/rules",
11
10
  contractsDir: ".qfai/contracts",
12
11
  uiContractsDir: ".qfai/contracts/ui",
13
12
  apiContractsDir: ".qfai/contracts/api",
@@ -31,7 +30,8 @@ var defaultConfig = {
31
30
  traceability: {
32
31
  brMustHaveSc: true,
33
32
  scMustTouchContracts: true,
34
- allowOrphanContracts: false
33
+ allowOrphanContracts: false,
34
+ unknownContractIdSeverity: "error"
35
35
  }
36
36
  },
37
37
  output: {
@@ -106,13 +106,6 @@ function normalizePaths(raw, configPath, issues) {
106
106
  configPath,
107
107
  issues
108
108
  ),
109
- rulesDir: readString(
110
- raw.rulesDir,
111
- base.rulesDir,
112
- "paths.rulesDir",
113
- configPath,
114
- issues
115
- ),
116
109
  contractsDir: readString(
117
110
  raw.contractsDir,
118
111
  base.contractsDir,
@@ -237,6 +230,13 @@ function normalizeValidation(raw, configPath, issues) {
237
230
  "validation.traceability.allowOrphanContracts",
238
231
  configPath,
239
232
  issues
233
+ ),
234
+ unknownContractIdSeverity: readTraceabilitySeverity(
235
+ traceabilityRaw?.unknownContractIdSeverity,
236
+ base.traceability.unknownContractIdSeverity,
237
+ "validation.traceability.unknownContractIdSeverity",
238
+ configPath,
239
+ issues
240
240
  )
241
241
  }
242
242
  };
@@ -316,6 +316,20 @@ function readFailOn(value, fallback, label, configPath, issues) {
316
316
  }
317
317
  return fallback;
318
318
  }
319
+ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
320
+ if (value === "warning" || value === "error") {
321
+ return value;
322
+ }
323
+ if (value !== void 0) {
324
+ issues.push(
325
+ configIssue(
326
+ configPath,
327
+ `${label} \u306F warning|error \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
328
+ )
329
+ );
330
+ }
331
+ return fallback;
332
+ }
319
333
  function readOutputFormat(value, fallback, label, configPath, issues) {
320
334
  if (value === "text" || value === "json" || value === "github") {
321
335
  return value;
@@ -356,13 +370,15 @@ function isRecord(value) {
356
370
  }
357
371
 
358
372
  // src/core/ids.ts
359
- var ID_PATTERNS = {
360
- SPEC: /\bSPEC-[A-Z0-9-]+\b/g,
361
- BR: /\bBR-[A-Z0-9-]+\b/g,
362
- SC: /\bSC-[A-Z0-9-]+\b/g,
363
- UI: /\bUI-[A-Z0-9-]+\b/g,
364
- API: /\bAPI-[A-Z0-9-]+\b/g,
365
- DATA: /\bDATA-[A-Z0-9-]+\b/g
373
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
374
+ var STRICT_ID_PATTERNS = {
375
+ SPEC: /\bSPEC-\d{4}\b/g,
376
+ BR: /\bBR-\d{4}\b/g,
377
+ SC: /\bSC-\d{4}\b/g,
378
+ UI: /\bUI-\d{4}\b/g,
379
+ API: /\bAPI-\d{4}\b/g,
380
+ DATA: /\bDATA-\d{4}\b/g,
381
+ ADR: /\bADR-\d{4}\b/g
366
382
  };
367
383
  var LOOSE_ID_PATTERNS = {
368
384
  SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
@@ -370,16 +386,17 @@ var LOOSE_ID_PATTERNS = {
370
386
  SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
371
387
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
372
388
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
373
- DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi
389
+ DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
390
+ ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
374
391
  };
375
392
  function extractIds(text, prefix) {
376
- const pattern = ID_PATTERNS[prefix];
393
+ const pattern = STRICT_ID_PATTERNS[prefix];
377
394
  const matches = text.match(pattern);
378
395
  return unique(matches ?? []);
379
396
  }
380
397
  function extractAllIds(text) {
381
398
  const all = [];
382
- Object.keys(ID_PATTERNS).forEach((prefix) => {
399
+ ID_PREFIXES.forEach((prefix) => {
383
400
  all.push(...extractIds(text, prefix));
384
401
  });
385
402
  return unique(all);
@@ -400,13 +417,13 @@ function unique(values) {
400
417
  return Array.from(new Set(values));
401
418
  }
402
419
  function isValidId(value, prefix) {
403
- const pattern = ID_PATTERNS[prefix];
420
+ const pattern = STRICT_ID_PATTERNS[prefix];
404
421
  const strict = new RegExp(pattern.source);
405
422
  return strict.test(value);
406
423
  }
407
424
 
408
425
  // src/core/report.ts
409
- import { readFile as readFile7 } from "fs/promises";
426
+ import { readFile as readFile9 } from "fs/promises";
410
427
 
411
428
  // src/core/discovery.ts
412
429
  import path3 from "path";
@@ -502,8 +519,8 @@ import { readFile as readFile2 } from "fs/promises";
502
519
  import path4 from "path";
503
520
  import { fileURLToPath } from "url";
504
521
  async function resolveToolVersion() {
505
- if ("0.2.6".length > 0) {
506
- return "0.2.6";
522
+ if ("0.2.9".length > 0) {
523
+ return "0.2.9";
507
524
  }
508
525
  try {
509
526
  const packagePath = resolvePackageJsonPath();
@@ -523,8 +540,50 @@ function resolvePackageJsonPath() {
523
540
 
524
541
  // src/core/validators/contracts.ts
525
542
  import { readFile as readFile3 } from "fs/promises";
543
+
544
+ // src/core/contracts.ts
526
545
  import path5 from "path";
527
546
  import { parse as parseYaml2 } from "yaml";
547
+ function parseStructuredContract(file, text) {
548
+ const ext = path5.extname(file).toLowerCase();
549
+ if (ext === ".json") {
550
+ return JSON.parse(text);
551
+ }
552
+ return parseYaml2(text);
553
+ }
554
+ function extractUiContractIds(doc) {
555
+ const id = typeof doc.id === "string" ? doc.id : "";
556
+ return extractIds(id, "UI");
557
+ }
558
+ function extractApiContractIds(doc) {
559
+ const operationIds = /* @__PURE__ */ new Set();
560
+ collectOperationIds(doc, operationIds);
561
+ const ids = /* @__PURE__ */ new Set();
562
+ for (const operationId of operationIds) {
563
+ extractIds(operationId, "API").forEach((id) => ids.add(id));
564
+ }
565
+ return Array.from(ids);
566
+ }
567
+ function collectOperationIds(value, out) {
568
+ if (!value || typeof value !== "object") {
569
+ return;
570
+ }
571
+ if (Array.isArray(value)) {
572
+ for (const item of value) {
573
+ collectOperationIds(item, out);
574
+ }
575
+ return;
576
+ }
577
+ for (const [key, entry] of Object.entries(value)) {
578
+ if (key === "operationId" && typeof entry === "string") {
579
+ out.add(entry);
580
+ continue;
581
+ }
582
+ collectOperationIds(entry, out);
583
+ }
584
+ }
585
+
586
+ // src/core/validators/contracts.ts
528
587
  var SQL_DANGEROUS_PATTERNS = [
529
588
  { pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
530
589
  { pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
@@ -573,12 +632,13 @@ async function validateUiContracts(uiRoot) {
573
632
  "SC",
574
633
  "UI",
575
634
  "API",
576
- "DATA"
635
+ "DATA",
636
+ "ADR"
577
637
  ]);
578
638
  if (invalidIds.length > 0) {
579
639
  issues.push(
580
640
  issue(
581
- "QFAI_ID_INVALID_FORMAT",
641
+ "QFAI-ID-002",
582
642
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
583
643
  "error",
584
644
  file,
@@ -587,30 +647,32 @@ async function validateUiContracts(uiRoot) {
587
647
  )
588
648
  );
589
649
  }
650
+ let doc;
590
651
  try {
591
- const doc = parseYaml2(text);
592
- const id = typeof doc.id === "string" ? doc.id : "";
593
- if (!id.startsWith("UI-")) {
594
- issues.push(
595
- issue(
596
- "QFAI-UI-001",
597
- "UI \u5951\u7D04\u306E id \u306F UI- \u3067\u59CB\u307E\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
598
- "error",
599
- file,
600
- "contracts.ui.id"
601
- )
602
- );
603
- }
652
+ doc = parseStructuredContract(file, text);
604
653
  } catch (error) {
605
654
  issues.push(
606
655
  issue(
607
- "QFAI-UI-002",
608
- `UI YAML \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError2(error)}`,
656
+ "QFAI-CONTRACT-001",
657
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
609
658
  "error",
610
659
  file,
611
660
  "contracts.ui.parse"
612
661
  )
613
662
  );
663
+ continue;
664
+ }
665
+ const uiIds = extractUiContractIds(doc);
666
+ if (uiIds.length === 0) {
667
+ issues.push(
668
+ issue(
669
+ "QFAI-CONTRACT-002",
670
+ `UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
671
+ "error",
672
+ file,
673
+ "contracts.ui.id"
674
+ )
675
+ );
614
676
  }
615
677
  }
616
678
  return issues;
@@ -637,12 +699,13 @@ async function validateApiContracts(apiRoot) {
637
699
  "SC",
638
700
  "UI",
639
701
  "API",
640
- "DATA"
702
+ "DATA",
703
+ "ADR"
641
704
  ]);
642
705
  if (invalidIds.length > 0) {
643
706
  issues.push(
644
707
  issue(
645
- "QFAI_ID_INVALID_FORMAT",
708
+ "QFAI-ID-002",
646
709
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
647
710
  "error",
648
711
  file,
@@ -651,29 +714,43 @@ async function validateApiContracts(apiRoot) {
651
714
  )
652
715
  );
653
716
  }
717
+ let doc;
654
718
  try {
655
- const doc = parseStructured(file, text);
656
- if (!doc || !hasOpenApi(doc)) {
657
- issues.push(
658
- issue(
659
- "QFAI-API-001",
660
- "OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
661
- "error",
662
- file,
663
- "contracts.api.openapi"
664
- )
665
- );
666
- }
719
+ doc = parseStructuredContract(file, text);
667
720
  } catch (error) {
668
721
  issues.push(
669
722
  issue(
670
- "QFAI-API-002",
671
- `API \u5B9A\u7FA9\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError2(error)}`,
723
+ "QFAI-CONTRACT-001",
724
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error)})`,
672
725
  "error",
673
726
  file,
674
727
  "contracts.api.parse"
675
728
  )
676
729
  );
730
+ continue;
731
+ }
732
+ if (!hasOpenApi(doc)) {
733
+ issues.push(
734
+ issue(
735
+ "QFAI-API-001",
736
+ "OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
737
+ "error",
738
+ file,
739
+ "contracts.api.openapi"
740
+ )
741
+ );
742
+ }
743
+ const apiIds = extractApiContractIds(doc);
744
+ if (apiIds.length === 0) {
745
+ issues.push(
746
+ issue(
747
+ "QFAI-CONTRACT-002",
748
+ `API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
749
+ "error",
750
+ file,
751
+ "contracts.api.id"
752
+ )
753
+ );
677
754
  }
678
755
  }
679
756
  return issues;
@@ -700,12 +777,13 @@ async function validateDataContracts(dataRoot) {
700
777
  "SC",
701
778
  "UI",
702
779
  "API",
703
- "DATA"
780
+ "DATA",
781
+ "ADR"
704
782
  ]);
705
783
  if (invalidIds.length > 0) {
706
784
  issues.push(
707
785
  issue(
708
- "QFAI_ID_INVALID_FORMAT",
786
+ "QFAI-ID-002",
709
787
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
710
788
  "error",
711
789
  file,
@@ -735,13 +813,6 @@ function lintSql(text, file) {
735
813
  }
736
814
  return issues;
737
815
  }
738
- function parseStructured(file, text) {
739
- const ext = path5.extname(file).toLowerCase();
740
- if (ext === ".json") {
741
- return JSON.parse(text);
742
- }
743
- return parseYaml2(text);
744
- }
745
816
  function hasOpenApi(doc) {
746
817
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
747
818
  }
@@ -752,25 +823,165 @@ function formatError2(error) {
752
823
  return String(error);
753
824
  }
754
825
  function issue(code, message, severity, file, rule, refs) {
755
- const issue5 = {
826
+ const issue6 = {
756
827
  code,
757
828
  severity,
758
829
  message
759
830
  };
760
831
  if (file) {
761
- issue5.file = file;
832
+ issue6.file = file;
762
833
  }
763
834
  if (rule) {
764
- issue5.rule = rule;
835
+ issue6.rule = rule;
765
836
  }
766
837
  if (refs && refs.length > 0) {
767
- issue5.refs = refs;
838
+ issue6.refs = refs;
768
839
  }
769
- return issue5;
840
+ return issue6;
770
841
  }
771
842
 
772
- // src/core/validators/scenario.ts
843
+ // src/core/validators/ids.ts
844
+ import { readFile as readFile5 } from "fs/promises";
845
+ import path6 from "path";
846
+
847
+ // src/core/contractIndex.ts
773
848
  import { readFile as readFile4 } from "fs/promises";
849
+ async function buildContractIndex(root, config) {
850
+ const uiRoot = resolvePath(root, config, "uiContractsDir");
851
+ const apiRoot = resolvePath(root, config, "apiContractsDir");
852
+ const dataRoot = resolvePath(root, config, "dataContractsDir");
853
+ const [uiFiles, apiFiles, dataFiles] = await Promise.all([
854
+ collectUiContractFiles(uiRoot),
855
+ collectApiContractFiles(apiRoot),
856
+ collectDataContractFiles(dataRoot)
857
+ ]);
858
+ const index = {
859
+ ids: /* @__PURE__ */ new Set(),
860
+ idToFiles: /* @__PURE__ */ new Map(),
861
+ files: { ui: uiFiles, api: apiFiles, data: dataFiles },
862
+ structuredParseFailedFiles: /* @__PURE__ */ new Set()
863
+ };
864
+ await indexUiContracts(uiFiles, index);
865
+ await indexApiContracts(apiFiles, index);
866
+ await indexDataContracts(dataFiles, index);
867
+ return index;
868
+ }
869
+ async function indexUiContracts(files, index) {
870
+ for (const file of files) {
871
+ const text = await readFile4(file, "utf-8");
872
+ try {
873
+ const doc = parseStructuredContract(file, text);
874
+ extractUiContractIds(doc).forEach((id) => record(index, id, file));
875
+ } catch {
876
+ index.structuredParseFailedFiles.add(file);
877
+ extractIds(text, "UI").forEach((id) => record(index, id, file));
878
+ }
879
+ }
880
+ }
881
+ async function indexApiContracts(files, index) {
882
+ for (const file of files) {
883
+ const text = await readFile4(file, "utf-8");
884
+ try {
885
+ const doc = parseStructuredContract(file, text);
886
+ extractApiContractIds(doc).forEach((id) => record(index, id, file));
887
+ } catch {
888
+ index.structuredParseFailedFiles.add(file);
889
+ extractIds(text, "API").forEach((id) => record(index, id, file));
890
+ }
891
+ }
892
+ }
893
+ async function indexDataContracts(files, index) {
894
+ for (const file of files) {
895
+ const text = await readFile4(file, "utf-8");
896
+ extractIds(text, "DATA").forEach((id) => record(index, id, file));
897
+ }
898
+ }
899
+ function record(index, id, file) {
900
+ index.ids.add(id);
901
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
902
+ current.add(file);
903
+ index.idToFiles.set(id, current);
904
+ }
905
+
906
+ // src/core/validators/ids.ts
907
+ async function validateDefinedIds(root, config) {
908
+ const issues = [];
909
+ const specRoot = resolvePath(root, config, "specDir");
910
+ const scenarioRoot = resolvePath(root, config, "scenariosDir");
911
+ const specFiles = await collectSpecFiles(specRoot);
912
+ const scenarioFiles = await collectFiles(scenarioRoot, {
913
+ extensions: [".feature"]
914
+ });
915
+ const defined = /* @__PURE__ */ new Map();
916
+ await collectSpecDefinitionIds(specFiles, defined);
917
+ await collectScenarioDefinitionIds(scenarioFiles, defined);
918
+ const contractIndex = await buildContractIndex(root, config);
919
+ for (const [id, files] of contractIndex.idToFiles.entries()) {
920
+ for (const file of files) {
921
+ recordId(defined, id, file);
922
+ }
923
+ }
924
+ for (const [id, files] of defined.entries()) {
925
+ if (files.size <= 1) {
926
+ continue;
927
+ }
928
+ const sorted = Array.from(files).sort();
929
+ issues.push(
930
+ issue2(
931
+ "QFAI-ID-001",
932
+ `ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
933
+ "error",
934
+ sorted[0],
935
+ "id.duplicate"
936
+ )
937
+ );
938
+ }
939
+ return issues;
940
+ }
941
+ async function collectSpecDefinitionIds(files, out) {
942
+ for (const file of files) {
943
+ const text = await readFile5(file, "utf-8");
944
+ extractIds(text, "SPEC").forEach((id) => recordId(out, id, file));
945
+ extractIds(text, "BR").forEach((id) => recordId(out, id, file));
946
+ }
947
+ }
948
+ async function collectScenarioDefinitionIds(files, out) {
949
+ for (const file of files) {
950
+ const text = await readFile5(file, "utf-8");
951
+ extractIds(text, "SC").forEach((id) => recordId(out, id, file));
952
+ }
953
+ }
954
+ function recordId(out, id, file) {
955
+ const current = out.get(id) ?? /* @__PURE__ */ new Set();
956
+ current.add(file);
957
+ out.set(id, current);
958
+ }
959
+ function formatFileList(files, root) {
960
+ return files.map((file) => {
961
+ const relative = path6.relative(root, file);
962
+ return relative.length > 0 ? relative : file;
963
+ }).join(", ");
964
+ }
965
+ function issue2(code, message, severity, file, rule, refs) {
966
+ const issue6 = {
967
+ code,
968
+ severity,
969
+ message
970
+ };
971
+ if (file) {
972
+ issue6.file = file;
973
+ }
974
+ if (rule) {
975
+ issue6.rule = rule;
976
+ }
977
+ if (refs && refs.length > 0) {
978
+ issue6.refs = refs;
979
+ }
980
+ return issue6;
981
+ }
982
+
983
+ // src/core/validators/scenario.ts
984
+ import { readFile as readFile6 } from "fs/promises";
774
985
  var GIVEN_PATTERN = /\bGiven\b/;
775
986
  var WHEN_PATTERN = /\bWhen\b/;
776
987
  var THEN_PATTERN = /\bThen\b/;
@@ -781,7 +992,7 @@ async function validateScenarios(root, config) {
781
992
  });
782
993
  if (files.length === 0) {
783
994
  return [
784
- issue2(
995
+ issue3(
785
996
  "QFAI-SC-000",
786
997
  "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
787
998
  "info",
@@ -792,7 +1003,7 @@ async function validateScenarios(root, config) {
792
1003
  }
793
1004
  const issues = [];
794
1005
  for (const file of files) {
795
- const text = await readFile4(file, "utf-8");
1006
+ const text = await readFile6(file, "utf-8");
796
1007
  issues.push(...validateScenarioContent(text, file));
797
1008
  }
798
1009
  return issues;
@@ -805,12 +1016,13 @@ function validateScenarioContent(text, file) {
805
1016
  "SC",
806
1017
  "UI",
807
1018
  "API",
808
- "DATA"
1019
+ "DATA",
1020
+ "ADR"
809
1021
  ]);
810
1022
  if (invalidIds.length > 0) {
811
1023
  issues.push(
812
- issue2(
813
- "QFAI_ID_INVALID_FORMAT",
1024
+ issue3(
1025
+ "QFAI-ID-002",
814
1026
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
815
1027
  "error",
816
1028
  file,
@@ -822,7 +1034,7 @@ function validateScenarioContent(text, file) {
822
1034
  const scIds = extractIds(text, "SC");
823
1035
  if (scIds.length === 0) {
824
1036
  issues.push(
825
- issue2(
1037
+ issue3(
826
1038
  "QFAI-SC-001",
827
1039
  "SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
828
1040
  "error",
@@ -834,7 +1046,7 @@ function validateScenarioContent(text, file) {
834
1046
  const specIds = extractIds(text, "SPEC");
835
1047
  if (specIds.length === 0) {
836
1048
  issues.push(
837
- issue2(
1049
+ issue3(
838
1050
  "QFAI-SC-002",
839
1051
  "SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
840
1052
  "error",
@@ -846,7 +1058,7 @@ function validateScenarioContent(text, file) {
846
1058
  const brIds = extractIds(text, "BR");
847
1059
  if (brIds.length === 0) {
848
1060
  issues.push(
849
- issue2(
1061
+ issue3(
850
1062
  "QFAI-SC-003",
851
1063
  "SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
852
1064
  "error",
@@ -867,7 +1079,7 @@ function validateScenarioContent(text, file) {
867
1079
  }
868
1080
  if (missingSteps.length > 0) {
869
1081
  issues.push(
870
- issue2(
1082
+ issue3(
871
1083
  "QFAI-SC-005",
872
1084
  `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
873
1085
  "warning",
@@ -878,33 +1090,33 @@ function validateScenarioContent(text, file) {
878
1090
  }
879
1091
  return issues;
880
1092
  }
881
- function issue2(code, message, severity, file, rule, refs) {
882
- const issue5 = {
1093
+ function issue3(code, message, severity, file, rule, refs) {
1094
+ const issue6 = {
883
1095
  code,
884
1096
  severity,
885
1097
  message
886
1098
  };
887
1099
  if (file) {
888
- issue5.file = file;
1100
+ issue6.file = file;
889
1101
  }
890
1102
  if (rule) {
891
- issue5.rule = rule;
1103
+ issue6.rule = rule;
892
1104
  }
893
1105
  if (refs && refs.length > 0) {
894
- issue5.refs = refs;
1106
+ issue6.refs = refs;
895
1107
  }
896
- return issue5;
1108
+ return issue6;
897
1109
  }
898
1110
 
899
1111
  // src/core/validators/spec.ts
900
- import { readFile as readFile5 } from "fs/promises";
1112
+ import { readFile as readFile7 } from "fs/promises";
901
1113
  async function validateSpecs(root, config) {
902
1114
  const specsRoot = resolvePath(root, config, "specDir");
903
1115
  const files = await collectSpecFiles(specsRoot);
904
1116
  if (files.length === 0) {
905
1117
  const expected = "spec-0001-<slug>.md";
906
1118
  return [
907
- issue3(
1119
+ issue4(
908
1120
  "QFAI-SPEC-000",
909
1121
  `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}`,
910
1122
  "info",
@@ -915,7 +1127,7 @@ async function validateSpecs(root, config) {
915
1127
  }
916
1128
  const issues = [];
917
1129
  for (const file of files) {
918
- const text = await readFile5(file, "utf-8");
1130
+ const text = await readFile7(file, "utf-8");
919
1131
  issues.push(
920
1132
  ...validateSpecContent(
921
1133
  text,
@@ -934,12 +1146,13 @@ function validateSpecContent(text, file, requiredSections) {
934
1146
  "SC",
935
1147
  "UI",
936
1148
  "API",
937
- "DATA"
1149
+ "DATA",
1150
+ "ADR"
938
1151
  ]);
939
1152
  if (invalidIds.length > 0) {
940
1153
  issues.push(
941
- issue3(
942
- "QFAI_ID_INVALID_FORMAT",
1154
+ issue4(
1155
+ "QFAI-ID-002",
943
1156
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
944
1157
  "error",
945
1158
  file,
@@ -951,7 +1164,7 @@ function validateSpecContent(text, file, requiredSections) {
951
1164
  const specIds = extractIds(text, "SPEC");
952
1165
  if (specIds.length === 0) {
953
1166
  issues.push(
954
- issue3(
1167
+ issue4(
955
1168
  "QFAI-SPEC-001",
956
1169
  "SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
957
1170
  "error",
@@ -963,7 +1176,7 @@ function validateSpecContent(text, file, requiredSections) {
963
1176
  const brIds = extractIds(text, "BR");
964
1177
  if (brIds.length === 0) {
965
1178
  issues.push(
966
- issue3(
1179
+ issue4(
967
1180
  "QFAI-SPEC-002",
968
1181
  "BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
969
1182
  "error",
@@ -975,7 +1188,7 @@ function validateSpecContent(text, file, requiredSections) {
975
1188
  const scIds = extractIds(text, "SC");
976
1189
  if (scIds.length > 0) {
977
1190
  issues.push(
978
- issue3(
1191
+ issue4(
979
1192
  "QFAI-SPEC-003",
980
1193
  "Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
981
1194
  "warning",
@@ -988,7 +1201,7 @@ function validateSpecContent(text, file, requiredSections) {
988
1201
  for (const section of requiredSections) {
989
1202
  if (!text.includes(section)) {
990
1203
  issues.push(
991
- issue3(
1204
+ issue4(
992
1205
  "QFAI-SPEC-004",
993
1206
  `\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
994
1207
  "error",
@@ -1000,26 +1213,26 @@ function validateSpecContent(text, file, requiredSections) {
1000
1213
  }
1001
1214
  return issues;
1002
1215
  }
1003
- function issue3(code, message, severity, file, rule, refs) {
1004
- const issue5 = {
1216
+ function issue4(code, message, severity, file, rule, refs) {
1217
+ const issue6 = {
1005
1218
  code,
1006
1219
  severity,
1007
1220
  message
1008
1221
  };
1009
1222
  if (file) {
1010
- issue5.file = file;
1223
+ issue6.file = file;
1011
1224
  }
1012
1225
  if (rule) {
1013
- issue5.rule = rule;
1226
+ issue6.rule = rule;
1014
1227
  }
1015
1228
  if (refs && refs.length > 0) {
1016
- issue5.refs = refs;
1229
+ issue6.refs = refs;
1017
1230
  }
1018
- return issue5;
1231
+ return issue6;
1019
1232
  }
1020
1233
 
1021
1234
  // src/core/validators/traceability.ts
1022
- import { readFile as readFile6 } from "fs/promises";
1235
+ import { readFile as readFile8 } from "fs/promises";
1023
1236
  async function validateTraceability(root, config) {
1024
1237
  const issues = [];
1025
1238
  const specsRoot = resolvePath(root, config, "specDir");
@@ -1035,36 +1248,141 @@ async function validateTraceability(root, config) {
1035
1248
  extensions: [".feature"]
1036
1249
  });
1037
1250
  const upstreamIds = /* @__PURE__ */ new Set();
1251
+ const specIds = /* @__PURE__ */ new Set();
1038
1252
  const brIdsInSpecs = /* @__PURE__ */ new Set();
1039
1253
  const brIdsInScenarios = /* @__PURE__ */ new Set();
1040
1254
  const scIdsInScenarios = /* @__PURE__ */ new Set();
1041
1255
  const scenarioContractIds = /* @__PURE__ */ new Set();
1042
1256
  const scWithContracts = /* @__PURE__ */ new Set();
1043
- for (const file of [...specFiles, ...decisionFiles]) {
1044
- const text = await readFile6(file, "utf-8");
1257
+ const specToBrIds = /* @__PURE__ */ new Map();
1258
+ const contractIndex = await buildContractIndex(root, config);
1259
+ const contractIds = contractIndex.ids;
1260
+ for (const file of specFiles) {
1261
+ const text = await readFile8(file, "utf-8");
1262
+ extractAllIds(text).forEach((id) => upstreamIds.add(id));
1263
+ const specIdsInFile = extractIds(text, "SPEC");
1264
+ specIdsInFile.forEach((id) => specIds.add(id));
1265
+ const brIds = extractIds(text, "BR");
1266
+ brIds.forEach((id) => brIdsInSpecs.add(id));
1267
+ const referencedContractIds = /* @__PURE__ */ new Set([
1268
+ ...extractIds(text, "UI"),
1269
+ ...extractIds(text, "API"),
1270
+ ...extractIds(text, "DATA")
1271
+ ]);
1272
+ const unknownContractIds = Array.from(referencedContractIds).filter(
1273
+ (id) => !contractIds.has(id)
1274
+ );
1275
+ if (unknownContractIds.length > 0) {
1276
+ issues.push(
1277
+ issue5(
1278
+ "QFAI-TRACE-009",
1279
+ `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1280
+ ", "
1281
+ )}`,
1282
+ "error",
1283
+ file,
1284
+ "traceability.specContractExists",
1285
+ unknownContractIds
1286
+ )
1287
+ );
1288
+ }
1289
+ for (const specId of specIdsInFile) {
1290
+ const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
1291
+ brIds.forEach((id) => current.add(id));
1292
+ specToBrIds.set(specId, current);
1293
+ }
1294
+ }
1295
+ for (const file of decisionFiles) {
1296
+ const text = await readFile8(file, "utf-8");
1045
1297
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1046
- extractIds(text, "BR").forEach((id) => brIdsInSpecs.add(id));
1047
1298
  }
1048
1299
  for (const file of scenarioFiles) {
1049
- const text = await readFile6(file, "utf-8");
1300
+ const text = await readFile8(file, "utf-8");
1050
1301
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1302
+ const specIdsInScenario = extractIds(text, "SPEC");
1051
1303
  const brIds = extractIds(text, "BR");
1052
- brIds.forEach((id) => brIdsInScenarios.add(id));
1053
1304
  const scIds = extractIds(text, "SC");
1054
- scIds.forEach((id) => scIdsInScenarios.add(id));
1055
- const contractIds = [
1305
+ const scenarioIds = [
1056
1306
  ...extractIds(text, "UI"),
1057
1307
  ...extractIds(text, "API"),
1058
1308
  ...extractIds(text, "DATA")
1059
1309
  ];
1060
- contractIds.forEach((id) => scenarioContractIds.add(id));
1061
- if (contractIds.length > 0) {
1310
+ brIds.forEach((id) => brIdsInScenarios.add(id));
1311
+ scIds.forEach((id) => scIdsInScenarios.add(id));
1312
+ scenarioIds.forEach((id) => scenarioContractIds.add(id));
1313
+ if (scenarioIds.length > 0) {
1062
1314
  scIds.forEach((id) => scWithContracts.add(id));
1063
1315
  }
1316
+ const unknownSpecIds = specIdsInScenario.filter((id) => !specIds.has(id));
1317
+ if (unknownSpecIds.length > 0) {
1318
+ issues.push(
1319
+ issue5(
1320
+ "QFAI-TRACE-005",
1321
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
1322
+ "error",
1323
+ file,
1324
+ "traceability.scenarioSpecExists",
1325
+ unknownSpecIds
1326
+ )
1327
+ );
1328
+ }
1329
+ const unknownBrIds = brIds.filter((id) => !brIdsInSpecs.has(id));
1330
+ if (unknownBrIds.length > 0) {
1331
+ issues.push(
1332
+ issue5(
1333
+ "QFAI-TRACE-006",
1334
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
1335
+ "error",
1336
+ file,
1337
+ "traceability.scenarioBrExists",
1338
+ unknownBrIds
1339
+ )
1340
+ );
1341
+ }
1342
+ const unknownContractIds = scenarioIds.filter((id) => !contractIds.has(id));
1343
+ if (unknownContractIds.length > 0) {
1344
+ issues.push(
1345
+ issue5(
1346
+ "QFAI-TRACE-008",
1347
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1348
+ ", "
1349
+ )}`,
1350
+ config.validation.traceability.unknownContractIdSeverity,
1351
+ file,
1352
+ "traceability.scenarioContractExists",
1353
+ unknownContractIds
1354
+ )
1355
+ );
1356
+ }
1357
+ if (specIdsInScenario.length > 0) {
1358
+ const allowedBrIds = /* @__PURE__ */ new Set();
1359
+ for (const specId of specIdsInScenario) {
1360
+ const brIdsForSpec = specToBrIds.get(specId);
1361
+ if (!brIdsForSpec) {
1362
+ continue;
1363
+ }
1364
+ brIdsForSpec.forEach((id) => allowedBrIds.add(id));
1365
+ }
1366
+ const invalidBrIds = brIds.filter((id) => !allowedBrIds.has(id));
1367
+ if (invalidBrIds.length > 0) {
1368
+ issues.push(
1369
+ issue5(
1370
+ "QFAI-TRACE-007",
1371
+ `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
1372
+ ", "
1373
+ )} (SPEC: ${specIdsInScenario.join(", ")})`,
1374
+ "error",
1375
+ file,
1376
+ "traceability.scenarioBrUnderSpec",
1377
+ invalidBrIds
1378
+ )
1379
+ );
1380
+ }
1381
+ }
1064
1382
  }
1065
1383
  if (upstreamIds.size === 0) {
1066
1384
  return [
1067
- issue4(
1385
+ issue5(
1068
1386
  "QFAI-TRACE-000",
1069
1387
  "\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1070
1388
  "info",
@@ -1079,7 +1397,7 @@ async function validateTraceability(root, config) {
1079
1397
  );
1080
1398
  if (orphanBrIds.length > 0) {
1081
1399
  issues.push(
1082
- issue4(
1400
+ issue5(
1083
1401
  "QFAI_TRACE_BR_ORPHAN",
1084
1402
  `BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
1085
1403
  "error",
@@ -1096,7 +1414,7 @@ async function validateTraceability(root, config) {
1096
1414
  );
1097
1415
  if (scWithoutContracts.length > 0) {
1098
1416
  issues.push(
1099
- issue4(
1417
+ issue5(
1100
1418
  "QFAI_TRACE_SC_NO_CONTRACT",
1101
1419
  `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
1102
1420
  ", "
@@ -1110,14 +1428,13 @@ async function validateTraceability(root, config) {
1110
1428
  }
1111
1429
  }
1112
1430
  if (!config.validation.traceability.allowOrphanContracts) {
1113
- const contractIds = await collectContractIds(root, config);
1114
1431
  if (contractIds.size > 0) {
1115
1432
  const orphanContracts = Array.from(contractIds).filter(
1116
1433
  (id) => !scenarioContractIds.has(id)
1117
1434
  );
1118
1435
  if (orphanContracts.length > 0) {
1119
1436
  issues.push(
1120
- issue4(
1437
+ issue5(
1121
1438
  "QFAI_CONTRACT_ORPHAN",
1122
1439
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1123
1440
  "error",
@@ -1134,27 +1451,6 @@ async function validateTraceability(root, config) {
1134
1451
  );
1135
1452
  return issues;
1136
1453
  }
1137
- async function collectContractIds(root, config) {
1138
- const contractIds = /* @__PURE__ */ new Set();
1139
- const uiRoot = resolvePath(root, config, "uiContractsDir");
1140
- const apiRoot = resolvePath(root, config, "apiContractsDir");
1141
- const dataRoot = resolvePath(root, config, "dataContractsDir");
1142
- const uiFiles = await collectUiContractFiles(uiRoot);
1143
- const apiFiles = await collectApiContractFiles(apiRoot);
1144
- const dataFiles = await collectDataContractFiles(dataRoot);
1145
- await collectIdsFromFiles(uiFiles, ["UI"], contractIds);
1146
- await collectIdsFromFiles(apiFiles, ["API"], contractIds);
1147
- await collectIdsFromFiles(dataFiles, ["DATA"], contractIds);
1148
- return contractIds;
1149
- }
1150
- async function collectIdsFromFiles(files, prefixes, out) {
1151
- for (const file of files) {
1152
- const text = await readFile6(file, "utf-8");
1153
- for (const prefix of prefixes) {
1154
- extractIds(text, prefix).forEach((id) => out.add(id));
1155
- }
1156
- }
1157
- }
1158
1454
  async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1159
1455
  const issues = [];
1160
1456
  const codeFiles = await collectFiles(srcRoot, {
@@ -1166,7 +1462,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1166
1462
  const targetFiles = [...codeFiles, ...testFiles];
1167
1463
  if (targetFiles.length === 0) {
1168
1464
  issues.push(
1169
- issue4(
1465
+ issue5(
1170
1466
  "QFAI-TRACE-001",
1171
1467
  "\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1172
1468
  "info",
@@ -1179,7 +1475,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1179
1475
  const pattern = buildIdPattern(Array.from(upstreamIds));
1180
1476
  let found = false;
1181
1477
  for (const file of targetFiles) {
1182
- const text = await readFile6(file, "utf-8");
1478
+ const text = await readFile8(file, "utf-8");
1183
1479
  if (pattern.test(text)) {
1184
1480
  found = true;
1185
1481
  break;
@@ -1187,7 +1483,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1187
1483
  }
1188
1484
  if (!found) {
1189
1485
  issues.push(
1190
- issue4(
1486
+ issue5(
1191
1487
  "QFAI-TRACE-002",
1192
1488
  "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
1193
1489
  "warning",
@@ -1202,22 +1498,22 @@ function buildIdPattern(ids) {
1202
1498
  const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1203
1499
  return new RegExp(`\\b(${escaped.join("|")})\\b`);
1204
1500
  }
1205
- function issue4(code, message, severity, file, rule, refs) {
1206
- const issue5 = {
1501
+ function issue5(code, message, severity, file, rule, refs) {
1502
+ const issue6 = {
1207
1503
  code,
1208
1504
  severity,
1209
1505
  message
1210
1506
  };
1211
1507
  if (file) {
1212
- issue5.file = file;
1508
+ issue6.file = file;
1213
1509
  }
1214
1510
  if (rule) {
1215
- issue5.rule = rule;
1511
+ issue6.rule = rule;
1216
1512
  }
1217
1513
  if (refs && refs.length > 0) {
1218
- issue5.refs = refs;
1514
+ issue6.refs = refs;
1219
1515
  }
1220
- return issue5;
1516
+ return issue6;
1221
1517
  }
1222
1518
 
1223
1519
  // src/core/validate.ts
@@ -1229,6 +1525,7 @@ async function validateProject(root, configResult) {
1229
1525
  ...await validateSpecs(root, config),
1230
1526
  ...await validateScenarios(root, config),
1231
1527
  ...await validateContracts(root, config),
1528
+ ...await validateDefinedIds(root, config),
1232
1529
  ...await validateTraceability(root, config)
1233
1530
  ];
1234
1531
  const toolVersion = await resolveToolVersion();
@@ -1241,8 +1538,8 @@ async function validateProject(root, configResult) {
1241
1538
  }
1242
1539
  function countIssues(issues) {
1243
1540
  return issues.reduce(
1244
- (acc, issue5) => {
1245
- acc[issue5.severity] += 1;
1541
+ (acc, issue6) => {
1542
+ acc[issue6.severity] += 1;
1246
1543
  return acc;
1247
1544
  },
1248
1545
  { info: 0, warning: 0, error: 0 }
@@ -1250,7 +1547,7 @@ function countIssues(issues) {
1250
1547
  }
1251
1548
 
1252
1549
  // src/core/report.ts
1253
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
1550
+ var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
1254
1551
  async function createReportData(root, validation, configResult) {
1255
1552
  const resolved = configResult ?? await loadConfig(root);
1256
1553
  const config = resolved.config;
@@ -1258,7 +1555,6 @@ async function createReportData(root, validation, configResult) {
1258
1555
  const specRoot = resolvePath(root, config, "specDir");
1259
1556
  const decisionsRoot = resolvePath(root, config, "decisionsDir");
1260
1557
  const scenariosRoot = resolvePath(root, config, "scenariosDir");
1261
- const rulesRoot = resolvePath(root, config, "rulesDir");
1262
1558
  const apiRoot = resolvePath(root, config, "apiContractsDir");
1263
1559
  const uiRoot = resolvePath(root, config, "uiContractsDir");
1264
1560
  const dbRoot = resolvePath(root, config, "dataContractsDir");
@@ -1271,7 +1567,6 @@ async function createReportData(root, validation, configResult) {
1271
1567
  const decisionFiles = await collectFiles(decisionsRoot, {
1272
1568
  extensions: [".md"]
1273
1569
  });
1274
- const ruleFiles = await collectFiles(rulesRoot, { extensions: [".md"] });
1275
1570
  const {
1276
1571
  api: apiFiles,
1277
1572
  ui: uiFiles,
@@ -1281,7 +1576,6 @@ async function createReportData(root, validation, configResult) {
1281
1576
  ...specFiles,
1282
1577
  ...scenarioFiles,
1283
1578
  ...decisionFiles,
1284
- ...ruleFiles,
1285
1579
  ...apiFiles,
1286
1580
  ...uiFiles,
1287
1581
  ...dbFiles
@@ -1307,7 +1601,6 @@ async function createReportData(root, validation, configResult) {
1307
1601
  specs: specFiles.length,
1308
1602
  scenarios: scenarioFiles.length,
1309
1603
  decisions: decisionFiles.length,
1310
- rules: ruleFiles.length,
1311
1604
  contracts: {
1312
1605
  api: apiFiles.length,
1313
1606
  ui: uiFiles.length,
@@ -1342,7 +1635,6 @@ function formatReportMarkdown(data) {
1342
1635
  lines.push(`- specs: ${data.summary.specs}`);
1343
1636
  lines.push(`- scenarios: ${data.summary.scenarios}`);
1344
1637
  lines.push(`- decisions: ${data.summary.decisions}`);
1345
- lines.push(`- rules: ${data.summary.rules}`);
1346
1638
  lines.push(
1347
1639
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
1348
1640
  );
@@ -1378,7 +1670,7 @@ function formatReportMarkdown(data) {
1378
1670
  lines.push("");
1379
1671
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
1380
1672
  const traceIssues = data.issues.filter(
1381
- (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code === "QFAI_CONTRACT_ORPHAN"
1673
+ (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-") || item.code === "QFAI_CONTRACT_ORPHAN"
1382
1674
  );
1383
1675
  if (traceIssues.length === 0) {
1384
1676
  lines.push("- (none)");
@@ -1418,8 +1710,8 @@ async function collectIds(files) {
1418
1710
  DATA: /* @__PURE__ */ new Set()
1419
1711
  };
1420
1712
  for (const file of files) {
1421
- const text = await readFile7(file, "utf-8");
1422
- for (const prefix of ID_PREFIXES) {
1713
+ const text = await readFile9(file, "utf-8");
1714
+ for (const prefix of ID_PREFIXES2) {
1423
1715
  const ids = extractIds(text, prefix);
1424
1716
  ids.forEach((id) => result[prefix].add(id));
1425
1717
  }
@@ -1436,7 +1728,7 @@ async function collectIds(files) {
1436
1728
  async function collectUpstreamIds(files) {
1437
1729
  const ids = /* @__PURE__ */ new Set();
1438
1730
  for (const file of files) {
1439
- const text = await readFile7(file, "utf-8");
1731
+ const text = await readFile9(file, "utf-8");
1440
1732
  extractAllIds(text).forEach((id) => ids.add(id));
1441
1733
  }
1442
1734
  return ids;
@@ -1457,7 +1749,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
1457
1749
  }
1458
1750
  const pattern = buildIdPattern2(Array.from(upstreamIds));
1459
1751
  for (const file of targetFiles) {
1460
- const text = await readFile7(file, "utf-8");
1752
+ const text = await readFile9(file, "utf-8");
1461
1753
  if (pattern.test(text)) {
1462
1754
  return true;
1463
1755
  }
@@ -1479,20 +1771,20 @@ function toSortedArray(values) {
1479
1771
  }
1480
1772
  function buildHotspots(issues) {
1481
1773
  const map = /* @__PURE__ */ new Map();
1482
- for (const issue5 of issues) {
1483
- if (!issue5.file) {
1774
+ for (const issue6 of issues) {
1775
+ if (!issue6.file) {
1484
1776
  continue;
1485
1777
  }
1486
- const current = map.get(issue5.file) ?? {
1487
- file: issue5.file,
1778
+ const current = map.get(issue6.file) ?? {
1779
+ file: issue6.file,
1488
1780
  total: 0,
1489
1781
  error: 0,
1490
1782
  warning: 0,
1491
1783
  info: 0
1492
1784
  };
1493
1785
  current.total += 1;
1494
- current[issue5.severity] += 1;
1495
- map.set(issue5.file, current);
1786
+ current[issue6.severity] += 1;
1787
+ map.set(issue6.file, current);
1496
1788
  }
1497
1789
  return Array.from(map.values()).sort(
1498
1790
  (a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
@@ -1513,6 +1805,7 @@ export {
1513
1805
  resolvePath,
1514
1806
  resolveToolVersion,
1515
1807
  validateContracts,
1808
+ validateDefinedIds,
1516
1809
  validateProject,
1517
1810
  validateScenarioContent,
1518
1811
  validateScenarios,