qfai 0.4.0 → 0.4.4

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 (77) hide show
  1. package/README.md +20 -1
  2. package/assets/init/.qfai/README.md +3 -1
  3. package/assets/init/.qfai/contracts/README.md +17 -8
  4. package/assets/init/.qfai/contracts/api/api-0001-sample.yaml +3 -2
  5. package/assets/init/.qfai/contracts/db/db-0001-sample.sql +2 -1
  6. package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +3 -1
  7. package/assets/init/.qfai/promptpack/modes/change.md +3 -2
  8. package/assets/init/.qfai/promptpack/modes/compatibility.md +2 -0
  9. package/assets/init/.qfai/prompts/README.md +1 -0
  10. package/assets/init/.qfai/prompts/qfai-generate-test-globs.md +29 -0
  11. package/assets/init/.qfai/prompts/require-to-spec.md +4 -2
  12. package/assets/init/.qfai/specs/README.md +9 -2
  13. package/assets/init/.qfai/specs/spec-0001/spec.md +2 -0
  14. package/assets/init/root/qfai.config.yaml +6 -1
  15. package/assets/init/root/tests/qfai-traceability.sample.test.ts +2 -2
  16. package/dist/cli/index.cjs +885 -489
  17. package/dist/cli/index.cjs.map +1 -1
  18. package/dist/cli/index.mjs +885 -489
  19. package/dist/cli/index.mjs.map +1 -1
  20. package/dist/core/config.d.ts +2 -1
  21. package/dist/core/config.d.ts.map +1 -1
  22. package/dist/core/config.js +4 -2
  23. package/dist/core/config.js.map +1 -1
  24. package/dist/core/contractIndex.d.ts +1 -2
  25. package/dist/core/contractIndex.d.ts.map +1 -1
  26. package/dist/core/contractIndex.js +10 -38
  27. package/dist/core/contractIndex.js.map +1 -1
  28. package/dist/core/contractsDecl.d.ts +3 -0
  29. package/dist/core/contractsDecl.d.ts.map +1 -0
  30. package/dist/core/contractsDecl.js +19 -0
  31. package/dist/core/contractsDecl.js.map +1 -0
  32. package/dist/core/fs.d.ts +5 -0
  33. package/dist/core/fs.d.ts.map +1 -1
  34. package/dist/core/fs.js +13 -0
  35. package/dist/core/fs.js.map +1 -1
  36. package/dist/core/ids.d.ts +1 -1
  37. package/dist/core/ids.d.ts.map +1 -1
  38. package/dist/core/ids.js +3 -3
  39. package/dist/core/ids.js.map +1 -1
  40. package/dist/core/parse/spec.d.ts +8 -0
  41. package/dist/core/parse/spec.d.ts.map +1 -1
  42. package/dist/core/parse/spec.js +43 -0
  43. package/dist/core/parse/spec.js.map +1 -1
  44. package/dist/core/report.d.ts +16 -2
  45. package/dist/core/report.d.ts.map +1 -1
  46. package/dist/core/report.js +144 -11
  47. package/dist/core/report.js.map +1 -1
  48. package/dist/core/scenarioModel.d.ts.map +1 -1
  49. package/dist/core/scenarioModel.js +3 -5
  50. package/dist/core/scenarioModel.js.map +1 -1
  51. package/dist/core/traceability.d.ts +15 -1
  52. package/dist/core/traceability.d.ts.map +1 -1
  53. package/dist/core/traceability.js +96 -9
  54. package/dist/core/traceability.js.map +1 -1
  55. package/dist/core/types.d.ts +6 -0
  56. package/dist/core/types.d.ts.map +1 -1
  57. package/dist/core/validate.d.ts.map +1 -1
  58. package/dist/core/validate.js +12 -1
  59. package/dist/core/validate.js.map +1 -1
  60. package/dist/core/validators/contracts.d.ts.map +1 -1
  61. package/dist/core/validators/contracts.js +45 -18
  62. package/dist/core/validators/contracts.js.map +1 -1
  63. package/dist/core/validators/scenario.d.ts.map +1 -1
  64. package/dist/core/validators/scenario.js +2 -15
  65. package/dist/core/validators/scenario.js.map +1 -1
  66. package/dist/core/validators/spec.js +1 -1
  67. package/dist/core/validators/spec.js.map +1 -1
  68. package/dist/core/validators/traceability.d.ts.map +1 -1
  69. package/dist/core/validators/traceability.js +66 -34
  70. package/dist/core/validators/traceability.js.map +1 -1
  71. package/dist/index.cjs +869 -473
  72. package/dist/index.cjs.map +1 -1
  73. package/dist/index.d.cts +37 -12
  74. package/dist/index.mjs +869 -473
  75. package/dist/index.mjs.map +1 -1
  76. package/dist/tsconfig.tsbuildinfo +1 -1
  77. package/package.json +2 -1
package/dist/index.cjs CHANGED
@@ -83,8 +83,9 @@ var defaultConfig = {
83
83
  },
84
84
  traceability: {
85
85
  brMustHaveSc: true,
86
- scMustTouchContracts: true,
87
86
  scMustHaveTest: true,
87
+ testFileGlobs: [],
88
+ testFileExcludeGlobs: [],
88
89
  scNoTestSeverity: "error",
89
90
  allowOrphanContracts: false,
90
91
  unknownContractIdSeverity: "error"
@@ -258,13 +259,6 @@ function normalizeValidation(raw, configPath, issues) {
258
259
  configPath,
259
260
  issues
260
261
  ),
261
- scMustTouchContracts: readBoolean(
262
- traceabilityRaw?.scMustTouchContracts,
263
- base.traceability.scMustTouchContracts,
264
- "validation.traceability.scMustTouchContracts",
265
- configPath,
266
- issues
267
- ),
268
262
  scMustHaveTest: readBoolean(
269
263
  traceabilityRaw?.scMustHaveTest,
270
264
  base.traceability.scMustHaveTest,
@@ -272,6 +266,20 @@ function normalizeValidation(raw, configPath, issues) {
272
266
  configPath,
273
267
  issues
274
268
  ),
269
+ testFileGlobs: readStringArray(
270
+ traceabilityRaw?.testFileGlobs,
271
+ base.traceability.testFileGlobs,
272
+ "validation.traceability.testFileGlobs",
273
+ configPath,
274
+ issues
275
+ ),
276
+ testFileExcludeGlobs: readStringArray(
277
+ traceabilityRaw?.testFileExcludeGlobs,
278
+ base.traceability.testFileExcludeGlobs,
279
+ "validation.traceability.testFileExcludeGlobs",
280
+ configPath,
281
+ issues
282
+ ),
275
283
  scNoTestSeverity: readTraceabilitySeverity(
276
284
  traceabilityRaw?.scNoTestSeverity,
277
285
  base.traceability.scNoTestSeverity,
@@ -404,14 +412,14 @@ function isRecord(value) {
404
412
  }
405
413
 
406
414
  // src/core/ids.ts
407
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
415
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
408
416
  var STRICT_ID_PATTERNS = {
409
417
  SPEC: /\bSPEC-\d{4}\b/g,
410
418
  BR: /\bBR-\d{4}\b/g,
411
419
  SC: /\bSC-\d{4}\b/g,
412
420
  UI: /\bUI-\d{4}\b/g,
413
421
  API: /\bAPI-\d{4}\b/g,
414
- DATA: /\bDATA-\d{4}\b/g,
422
+ DB: /\bDB-\d{4}\b/g,
415
423
  ADR: /\bADR-\d{4}\b/g
416
424
  };
417
425
  var LOOSE_ID_PATTERNS = {
@@ -420,7 +428,7 @@ var LOOSE_ID_PATTERNS = {
420
428
  SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
421
429
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
422
430
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
423
- DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
431
+ DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
424
432
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
425
433
  };
426
434
  function extractIds(text, prefix) {
@@ -458,7 +466,11 @@ function isValidId(value, prefix) {
458
466
 
459
467
  // src/core/report.ts
460
468
  var import_promises14 = require("fs/promises");
461
- var import_node_path10 = __toESM(require("path"), 1);
469
+ var import_node_path11 = __toESM(require("path"), 1);
470
+
471
+ // src/core/contractIndex.ts
472
+ var import_promises5 = require("fs/promises");
473
+ var import_node_path4 = __toESM(require("path"), 1);
462
474
 
463
475
  // src/core/discovery.ts
464
476
  var import_promises4 = require("fs/promises");
@@ -466,6 +478,7 @@ var import_promises4 = require("fs/promises");
466
478
  // src/core/fs.ts
467
479
  var import_promises2 = require("fs/promises");
468
480
  var import_node_path2 = __toESM(require("path"), 1);
481
+ var import_fast_glob = __toESM(require("fast-glob"), 1);
469
482
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
470
483
  "node_modules",
471
484
  ".git",
@@ -487,6 +500,18 @@ async function collectFiles(root, options = {}) {
487
500
  await walk(root, root, ignoreDirs, extensions, entries);
488
501
  return entries;
489
502
  }
503
+ async function collectFilesByGlobs(root, options) {
504
+ if (options.globs.length === 0) {
505
+ return [];
506
+ }
507
+ return (0, import_fast_glob.default)(options.globs, {
508
+ cwd: root,
509
+ ignore: options.ignore ?? [],
510
+ onlyFiles: true,
511
+ absolute: true,
512
+ unique: true
513
+ });
514
+ }
490
515
  async function walk(base, current, ignoreDirs, extensions, out) {
491
516
  const items = await (0, import_promises2.readdir)(current, { withFileTypes: true });
492
517
  for (const item of items) {
@@ -598,8 +623,221 @@ async function exists2(target) {
598
623
  }
599
624
  }
600
625
 
626
+ // src/core/contractsDecl.ts
627
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
628
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
629
+ function extractDeclaredContractIds(text) {
630
+ const ids = [];
631
+ for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
632
+ const id = match[1];
633
+ if (id) {
634
+ ids.push(id);
635
+ }
636
+ }
637
+ return ids;
638
+ }
639
+ function stripContractDeclarationLines(text) {
640
+ return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
641
+ }
642
+
643
+ // src/core/contractIndex.ts
644
+ async function buildContractIndex(root, config) {
645
+ const contractsRoot = resolvePath(root, config, "contractsDir");
646
+ const uiRoot = import_node_path4.default.join(contractsRoot, "ui");
647
+ const apiRoot = import_node_path4.default.join(contractsRoot, "api");
648
+ const dbRoot = import_node_path4.default.join(contractsRoot, "db");
649
+ const [uiFiles, apiFiles, dbFiles] = await Promise.all([
650
+ collectUiContractFiles(uiRoot),
651
+ collectApiContractFiles(apiRoot),
652
+ collectDataContractFiles(dbRoot)
653
+ ]);
654
+ const index = {
655
+ ids: /* @__PURE__ */ new Set(),
656
+ idToFiles: /* @__PURE__ */ new Map(),
657
+ files: { ui: uiFiles, api: apiFiles, db: dbFiles }
658
+ };
659
+ await indexContractFiles(uiFiles, index);
660
+ await indexContractFiles(apiFiles, index);
661
+ await indexContractFiles(dbFiles, index);
662
+ return index;
663
+ }
664
+ async function indexContractFiles(files, index) {
665
+ for (const file of files) {
666
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
667
+ extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
668
+ }
669
+ }
670
+ function record(index, id, file) {
671
+ index.ids.add(id);
672
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
673
+ current.add(file);
674
+ index.idToFiles.set(id, current);
675
+ }
676
+
677
+ // src/core/parse/markdown.ts
678
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
679
+ function parseHeadings(md) {
680
+ const lines = md.split(/\r?\n/);
681
+ const headings = [];
682
+ for (let i = 0; i < lines.length; i++) {
683
+ const line = lines[i] ?? "";
684
+ const match = line.match(HEADING_RE);
685
+ if (!match) continue;
686
+ const levelToken = match[1];
687
+ const title = match[2];
688
+ if (!levelToken || !title) continue;
689
+ headings.push({
690
+ level: levelToken.length,
691
+ title: title.trim(),
692
+ line: i + 1
693
+ });
694
+ }
695
+ return headings;
696
+ }
697
+ function extractH2Sections(md) {
698
+ const lines = md.split(/\r?\n/);
699
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
700
+ const sections = /* @__PURE__ */ new Map();
701
+ for (let i = 0; i < headings.length; i++) {
702
+ const current = headings[i];
703
+ if (!current) continue;
704
+ const next = headings[i + 1];
705
+ const startLine = current.line + 1;
706
+ const endLine = (next?.line ?? lines.length + 1) - 1;
707
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
708
+ sections.set(current.title.trim(), {
709
+ title: current.title.trim(),
710
+ startLine,
711
+ endLine,
712
+ body
713
+ });
714
+ }
715
+ return sections;
716
+ }
717
+
718
+ // src/core/parse/spec.ts
719
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
720
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
721
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
722
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
723
+ var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
724
+ var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
725
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
726
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
727
+ function parseSpec(md, file) {
728
+ const headings = parseHeadings(md);
729
+ const h1 = headings.find((heading) => heading.level === 1);
730
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
731
+ const sections = extractH2Sections(md);
732
+ const sectionNames = new Set(Array.from(sections.keys()));
733
+ const brSection = sections.get(BR_SECTION_TITLE);
734
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
735
+ const startLine = brSection?.startLine ?? 1;
736
+ const brs = [];
737
+ const brsWithoutPriority = [];
738
+ const brsWithInvalidPriority = [];
739
+ for (let i = 0; i < brLines.length; i++) {
740
+ const lineText = brLines[i] ?? "";
741
+ const lineNumber = startLine + i;
742
+ const validMatch = lineText.match(BR_LINE_RE);
743
+ if (validMatch) {
744
+ const id = validMatch[1];
745
+ const priority = validMatch[2];
746
+ const text = validMatch[3];
747
+ if (!id || !priority || !text) continue;
748
+ brs.push({
749
+ id,
750
+ priority,
751
+ text: text.trim(),
752
+ line: lineNumber
753
+ });
754
+ continue;
755
+ }
756
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
757
+ if (anyPriorityMatch) {
758
+ const id = anyPriorityMatch[1];
759
+ const priority = anyPriorityMatch[2];
760
+ const text = anyPriorityMatch[3];
761
+ if (!id || !priority || !text) continue;
762
+ if (!VALID_PRIORITIES.has(priority)) {
763
+ brsWithInvalidPriority.push({
764
+ id,
765
+ priority,
766
+ text: text.trim(),
767
+ line: lineNumber
768
+ });
769
+ }
770
+ continue;
771
+ }
772
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
773
+ if (noPriorityMatch) {
774
+ const id = noPriorityMatch[1];
775
+ const text = noPriorityMatch[2];
776
+ if (!id || !text) continue;
777
+ brsWithoutPriority.push({
778
+ id,
779
+ text: text.trim(),
780
+ line: lineNumber
781
+ });
782
+ }
783
+ }
784
+ const parsed = {
785
+ file,
786
+ sections: sectionNames,
787
+ brs,
788
+ brsWithoutPriority,
789
+ brsWithInvalidPriority,
790
+ contractRefs: parseContractRefs(md)
791
+ };
792
+ if (specId) {
793
+ parsed.specId = specId;
794
+ }
795
+ return parsed;
796
+ }
797
+ function parseContractRefs(md) {
798
+ const lines = [];
799
+ for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
800
+ lines.push((match[1] ?? "").trim());
801
+ }
802
+ const ids = [];
803
+ const invalidTokens = [];
804
+ let hasNone = false;
805
+ for (const line of lines) {
806
+ if (line.length === 0) {
807
+ invalidTokens.push("(empty)");
808
+ continue;
809
+ }
810
+ const tokens = line.split(",").map((token) => token.trim());
811
+ for (const token of tokens) {
812
+ if (token.length === 0) {
813
+ invalidTokens.push("(empty)");
814
+ continue;
815
+ }
816
+ if (token === "none") {
817
+ hasNone = true;
818
+ continue;
819
+ }
820
+ if (CONTRACT_REF_ID_RE.test(token)) {
821
+ ids.push(token);
822
+ continue;
823
+ }
824
+ invalidTokens.push(token);
825
+ }
826
+ }
827
+ return {
828
+ lines,
829
+ ids: unique2(ids),
830
+ invalidTokens: unique2(invalidTokens),
831
+ hasNone
832
+ };
833
+ }
834
+ function unique2(values) {
835
+ return Array.from(new Set(values));
836
+ }
837
+
601
838
  // src/core/traceability.ts
602
- var import_promises5 = require("fs/promises");
839
+ var import_promises6 = require("fs/promises");
840
+ var import_node_path5 = __toESM(require("path"), 1);
603
841
 
604
842
  // src/core/gherkin/parse.ts
605
843
  var import_gherkin = require("@cucumber/gherkin");
@@ -632,7 +870,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
632
870
  var BR_TAG_RE = /^BR-\d{4}$/;
633
871
  var UI_TAG_RE = /^UI-\d{4}$/;
634
872
  var API_TAG_RE = /^API-\d{4}$/;
635
- var DATA_TAG_RE = /^DATA-\d{4}$/;
873
+ var DB_TAG_RE = /^DB-\d{4}$/;
636
874
  function parseScenarioDocument(text, uri) {
637
875
  const { gherkinDocument, errors } = parseGherkin(text, uri);
638
876
  if (!gherkinDocument) {
@@ -661,10 +899,10 @@ function buildScenarioAtoms(document) {
661
899
  return document.scenarios.map((scenario) => {
662
900
  const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
663
901
  const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
664
- const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
902
+ const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
665
903
  const contractIds = /* @__PURE__ */ new Set();
666
904
  scenario.tags.forEach((tag) => {
667
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
905
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
668
906
  contractIds.add(tag);
669
907
  }
670
908
  });
@@ -672,7 +910,7 @@ function buildScenarioAtoms(document) {
672
910
  for (const text of collectStepTexts(step)) {
673
911
  extractIds(text, "UI").forEach((id) => contractIds.add(id));
674
912
  extractIds(text, "API").forEach((id) => contractIds.add(id));
675
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
913
+ extractIds(text, "DB").forEach((id) => contractIds.add(id));
676
914
  }
677
915
  }
678
916
  const atom = {
@@ -751,16 +989,37 @@ function collectStepTexts(step) {
751
989
  }
752
990
  return texts;
753
991
  }
754
- function unique2(values) {
992
+ function unique3(values) {
755
993
  return Array.from(new Set(values));
756
994
  }
757
995
 
758
996
  // src/core/traceability.ts
759
997
  var SC_TAG_RE2 = /^SC-\d{4}$/;
998
+ var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
999
+ var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
1000
+ "**/node_modules/**",
1001
+ "**/.git/**",
1002
+ "**/.qfai/**",
1003
+ "**/dist/**",
1004
+ "**/build/**",
1005
+ "**/coverage/**",
1006
+ "**/.next/**",
1007
+ "**/out/**"
1008
+ ];
1009
+ function extractAnnotatedScIds(text) {
1010
+ const ids = /* @__PURE__ */ new Set();
1011
+ for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
1012
+ const suffix = match[1];
1013
+ if (suffix) {
1014
+ ids.add(`SC-${suffix}`);
1015
+ }
1016
+ }
1017
+ return Array.from(ids);
1018
+ }
760
1019
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
761
1020
  const scIds = /* @__PURE__ */ new Set();
762
1021
  for (const file of scenarioFiles) {
763
- const text = await (0, import_promises5.readFile)(file, "utf-8");
1022
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
764
1023
  const { document, errors } = parseScenarioDocument(text, file);
765
1024
  if (!document || errors.length > 0) {
766
1025
  continue;
@@ -775,14 +1034,67 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
775
1034
  }
776
1035
  return scIds;
777
1036
  }
778
- async function collectScTestReferences(testsRoot) {
1037
+ async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
1038
+ const sources = /* @__PURE__ */ new Map();
1039
+ for (const file of scenarioFiles) {
1040
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
1041
+ const { document, errors } = parseScenarioDocument(text, file);
1042
+ if (!document || errors.length > 0) {
1043
+ continue;
1044
+ }
1045
+ for (const scenario of document.scenarios) {
1046
+ for (const tag of scenario.tags) {
1047
+ if (!SC_TAG_RE2.test(tag)) {
1048
+ continue;
1049
+ }
1050
+ const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
1051
+ current.add(file);
1052
+ sources.set(tag, current);
1053
+ }
1054
+ }
1055
+ }
1056
+ return sources;
1057
+ }
1058
+ async function collectScTestReferences(root, globs, excludeGlobs) {
779
1059
  const refs = /* @__PURE__ */ new Map();
780
- const testFiles = await collectFiles(testsRoot, {
781
- extensions: [".ts", ".tsx", ".js", ".jsx"]
782
- });
783
- for (const file of testFiles) {
784
- const text = await (0, import_promises5.readFile)(file, "utf-8");
785
- const scIds = extractIds(text, "SC");
1060
+ const normalizedGlobs = normalizeGlobs(globs);
1061
+ const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
1062
+ const mergedExcludeGlobs = Array.from(
1063
+ /* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
1064
+ );
1065
+ if (normalizedGlobs.length === 0) {
1066
+ return {
1067
+ refs,
1068
+ scan: {
1069
+ globs: normalizedGlobs,
1070
+ excludeGlobs: mergedExcludeGlobs,
1071
+ matchedFileCount: 0
1072
+ }
1073
+ };
1074
+ }
1075
+ let files = [];
1076
+ try {
1077
+ files = await collectFilesByGlobs(root, {
1078
+ globs: normalizedGlobs,
1079
+ ignore: mergedExcludeGlobs
1080
+ });
1081
+ } catch (error) {
1082
+ return {
1083
+ refs,
1084
+ scan: {
1085
+ globs: normalizedGlobs,
1086
+ excludeGlobs: mergedExcludeGlobs,
1087
+ matchedFileCount: 0
1088
+ },
1089
+ error: formatError3(error)
1090
+ };
1091
+ }
1092
+ const normalizedFiles = Array.from(
1093
+ new Set(files.map((file) => import_node_path5.default.normalize(file)))
1094
+ );
1095
+ for (const file of normalizedFiles) {
1096
+ const text = await (0, import_promises6.readFile)(file, "utf-8");
1097
+ const scIds = extractAnnotatedScIds(text);
786
1098
  if (scIds.length === 0) {
787
1099
  continue;
788
1100
  }
@@ -792,7 +1104,14 @@ async function collectScTestReferences(testsRoot) {
792
1104
  refs.set(scId, current);
793
1105
  }
794
1106
  }
795
- return refs;
1107
+ return {
1108
+ refs,
1109
+ scan: {
1110
+ globs: normalizedGlobs,
1111
+ excludeGlobs: mergedExcludeGlobs,
1112
+ matchedFileCount: normalizedFiles.length
1113
+ }
1114
+ };
796
1115
  }
797
1116
  function buildScCoverage(scIds, refs) {
798
1117
  const sortedScIds = toSortedArray(scIds);
@@ -820,18 +1139,27 @@ function buildScCoverage(scIds, refs) {
820
1139
  function toSortedArray(values) {
821
1140
  return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
822
1141
  }
1142
+ function normalizeGlobs(globs) {
1143
+ return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1144
+ }
1145
+ function formatError3(error) {
1146
+ if (error instanceof Error) {
1147
+ return error.message;
1148
+ }
1149
+ return String(error);
1150
+ }
823
1151
 
824
1152
  // src/core/version.ts
825
- var import_promises6 = require("fs/promises");
826
- var import_node_path4 = __toESM(require("path"), 1);
1153
+ var import_promises7 = require("fs/promises");
1154
+ var import_node_path6 = __toESM(require("path"), 1);
827
1155
  var import_node_url = require("url");
828
1156
  async function resolveToolVersion() {
829
- if ("0.4.0".length > 0) {
830
- return "0.4.0";
1157
+ if ("0.4.4".length > 0) {
1158
+ return "0.4.4";
831
1159
  }
832
1160
  try {
833
1161
  const packagePath = resolvePackageJsonPath();
834
- const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
1162
+ const raw = await (0, import_promises7.readFile)(packagePath, "utf-8");
835
1163
  const parsed = JSON.parse(raw);
836
1164
  const version = typeof parsed.version === "string" ? parsed.version : "";
837
1165
  return version.length > 0 ? version : "unknown";
@@ -842,54 +1170,23 @@ async function resolveToolVersion() {
842
1170
  function resolvePackageJsonPath() {
843
1171
  const base = __filename;
844
1172
  const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
845
- return import_node_path4.default.resolve(import_node_path4.default.dirname(basePath), "../../package.json");
1173
+ return import_node_path6.default.resolve(import_node_path6.default.dirname(basePath), "../../package.json");
846
1174
  }
847
1175
 
848
1176
  // src/core/validators/contracts.ts
849
- var import_promises7 = require("fs/promises");
850
- var import_node_path6 = __toESM(require("path"), 1);
1177
+ var import_promises8 = require("fs/promises");
1178
+ var import_node_path8 = __toESM(require("path"), 1);
851
1179
 
852
1180
  // src/core/contracts.ts
853
- var import_node_path5 = __toESM(require("path"), 1);
1181
+ var import_node_path7 = __toESM(require("path"), 1);
854
1182
  var import_yaml2 = require("yaml");
855
1183
  function parseStructuredContract(file, text) {
856
- const ext = import_node_path5.default.extname(file).toLowerCase();
1184
+ const ext = import_node_path7.default.extname(file).toLowerCase();
857
1185
  if (ext === ".json") {
858
1186
  return JSON.parse(text);
859
1187
  }
860
1188
  return (0, import_yaml2.parse)(text);
861
1189
  }
862
- function extractUiContractIds(doc) {
863
- const id = typeof doc.id === "string" ? doc.id : "";
864
- return extractIds(id, "UI");
865
- }
866
- function extractApiContractIds(doc) {
867
- const operationIds = /* @__PURE__ */ new Set();
868
- collectOperationIds(doc, operationIds);
869
- const ids = /* @__PURE__ */ new Set();
870
- for (const operationId of operationIds) {
871
- extractIds(operationId, "API").forEach((id) => ids.add(id));
872
- }
873
- return Array.from(ids);
874
- }
875
- function collectOperationIds(value, out) {
876
- if (!value || typeof value !== "object") {
877
- return;
878
- }
879
- if (Array.isArray(value)) {
880
- for (const item of value) {
881
- collectOperationIds(item, out);
882
- }
883
- return;
884
- }
885
- for (const [key, entry] of Object.entries(value)) {
886
- if (key === "operationId" && typeof entry === "string") {
887
- out.add(entry);
888
- continue;
889
- }
890
- collectOperationIds(entry, out);
891
- }
892
- }
893
1190
 
894
1191
  // src/core/validators/contracts.ts
895
1192
  var SQL_DANGEROUS_PATTERNS = [
@@ -904,9 +1201,11 @@ var SQL_DANGEROUS_PATTERNS = [
904
1201
  async function validateContracts(root, config) {
905
1202
  const issues = [];
906
1203
  const contractsRoot = resolvePath(root, config, "contractsDir");
907
- issues.push(...await validateUiContracts(import_node_path6.default.join(contractsRoot, "ui")));
908
- issues.push(...await validateApiContracts(import_node_path6.default.join(contractsRoot, "api")));
909
- issues.push(...await validateDataContracts(import_node_path6.default.join(contractsRoot, "db")));
1204
+ issues.push(...await validateUiContracts(import_node_path8.default.join(contractsRoot, "ui")));
1205
+ issues.push(...await validateApiContracts(import_node_path8.default.join(contractsRoot, "api")));
1206
+ issues.push(...await validateDataContracts(import_node_path8.default.join(contractsRoot, "db")));
1207
+ const contractIndex = await buildContractIndex(root, config);
1208
+ issues.push(...validateDuplicateContractIds(contractIndex));
910
1209
  return issues;
911
1210
  }
912
1211
  async function validateUiContracts(uiRoot) {
@@ -924,14 +1223,14 @@ async function validateUiContracts(uiRoot) {
924
1223
  }
925
1224
  const issues = [];
926
1225
  for (const file of files) {
927
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1226
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
928
1227
  const invalidIds = extractInvalidIds(text, [
929
1228
  "SPEC",
930
1229
  "BR",
931
1230
  "SC",
932
1231
  "UI",
933
1232
  "API",
934
- "DATA",
1233
+ "DB",
935
1234
  "ADR"
936
1235
  ]);
937
1236
  if (invalidIds.length > 0) {
@@ -946,32 +1245,20 @@ async function validateUiContracts(uiRoot) {
946
1245
  )
947
1246
  );
948
1247
  }
949
- let doc;
1248
+ const declaredIds = extractDeclaredContractIds(text);
1249
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
950
1250
  try {
951
- doc = parseStructuredContract(file, text);
1251
+ parseStructuredContract(file, stripContractDeclarationLines(text));
952
1252
  } catch (error) {
953
1253
  issues.push(
954
1254
  issue(
955
1255
  "QFAI-CONTRACT-001",
956
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
1256
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
957
1257
  "error",
958
1258
  file,
959
1259
  "contracts.ui.parse"
960
1260
  )
961
1261
  );
962
- continue;
963
- }
964
- const uiIds = extractUiContractIds(doc);
965
- if (uiIds.length === 0) {
966
- issues.push(
967
- issue(
968
- "QFAI-CONTRACT-002",
969
- `UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
970
- "error",
971
- file,
972
- "contracts.ui.id"
973
- )
974
- );
975
1262
  }
976
1263
  }
977
1264
  return issues;
@@ -991,14 +1278,14 @@ async function validateApiContracts(apiRoot) {
991
1278
  }
992
1279
  const issues = [];
993
1280
  for (const file of files) {
994
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1281
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
995
1282
  const invalidIds = extractInvalidIds(text, [
996
1283
  "SPEC",
997
1284
  "BR",
998
1285
  "SC",
999
1286
  "UI",
1000
1287
  "API",
1001
- "DATA",
1288
+ "DB",
1002
1289
  "ADR"
1003
1290
  ]);
1004
1291
  if (invalidIds.length > 0) {
@@ -1013,14 +1300,16 @@ async function validateApiContracts(apiRoot) {
1013
1300
  )
1014
1301
  );
1015
1302
  }
1303
+ const declaredIds = extractDeclaredContractIds(text);
1304
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
1016
1305
  let doc;
1017
1306
  try {
1018
- doc = parseStructuredContract(file, text);
1307
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
1019
1308
  } catch (error) {
1020
1309
  issues.push(
1021
1310
  issue(
1022
1311
  "QFAI-CONTRACT-001",
1023
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
1312
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
1024
1313
  "error",
1025
1314
  file,
1026
1315
  "contracts.api.parse"
@@ -1039,18 +1328,6 @@ async function validateApiContracts(apiRoot) {
1039
1328
  )
1040
1329
  );
1041
1330
  }
1042
- const apiIds = extractApiContractIds(doc);
1043
- if (apiIds.length === 0) {
1044
- issues.push(
1045
- issue(
1046
- "QFAI-CONTRACT-002",
1047
- `API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
1048
- "error",
1049
- file,
1050
- "contracts.api.id"
1051
- )
1052
- );
1053
- }
1054
1331
  }
1055
1332
  return issues;
1056
1333
  }
@@ -1059,24 +1336,24 @@ async function validateDataContracts(dataRoot) {
1059
1336
  if (files.length === 0) {
1060
1337
  return [
1061
1338
  issue(
1062
- "QFAI-DATA-000",
1063
- "DATA \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1339
+ "QFAI-DB-000",
1340
+ "DB \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1064
1341
  "info",
1065
1342
  dataRoot,
1066
- "contracts.data.files"
1343
+ "contracts.db.files"
1067
1344
  )
1068
1345
  ];
1069
1346
  }
1070
1347
  const issues = [];
1071
1348
  for (const file of files) {
1072
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1349
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1073
1350
  const invalidIds = extractInvalidIds(text, [
1074
1351
  "SPEC",
1075
1352
  "BR",
1076
1353
  "SC",
1077
1354
  "UI",
1078
1355
  "API",
1079
- "DATA",
1356
+ "DB",
1080
1357
  "ADR"
1081
1358
  ]);
1082
1359
  if (invalidIds.length > 0) {
@@ -1091,6 +1368,8 @@ async function validateDataContracts(dataRoot) {
1091
1368
  )
1092
1369
  );
1093
1370
  }
1371
+ const declaredIds = extractDeclaredContractIds(text);
1372
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
1094
1373
  issues.push(...lintSql(text, file));
1095
1374
  }
1096
1375
  return issues;
@@ -1101,21 +1380,87 @@ function lintSql(text, file) {
1101
1380
  if (pattern.test(text)) {
1102
1381
  issues.push(
1103
1382
  issue(
1104
- "QFAI-DATA-001",
1383
+ "QFAI-DB-001",
1105
1384
  `\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
1106
1385
  "warning",
1107
1386
  file,
1108
- "contracts.data.sql"
1387
+ "contracts.db.sql"
1109
1388
  )
1110
1389
  );
1111
1390
  }
1112
1391
  }
1113
1392
  return issues;
1114
1393
  }
1394
+ function validateDeclaredContractIds(ids, file, kind) {
1395
+ const issues = [];
1396
+ if (ids.length === 0) {
1397
+ issues.push(
1398
+ issue(
1399
+ "QFAI-CONTRACT-010",
1400
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
1401
+ "error",
1402
+ file,
1403
+ "contracts.declaration"
1404
+ )
1405
+ );
1406
+ return issues;
1407
+ }
1408
+ if (ids.length > 1) {
1409
+ issues.push(
1410
+ issue(
1411
+ "QFAI-CONTRACT-011",
1412
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B\u8907\u6570\u306E QFAI-CONTRACT-ID \u304C\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${ids.join(
1413
+ ", "
1414
+ )}`,
1415
+ "error",
1416
+ file,
1417
+ "contracts.declaration",
1418
+ ids
1419
+ )
1420
+ );
1421
+ return issues;
1422
+ }
1423
+ const [id] = ids;
1424
+ if (id && !id.startsWith(`${kind}-`)) {
1425
+ issues.push(
1426
+ issue(
1427
+ "QFAI-CONTRACT-013",
1428
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E QFAI-CONTRACT-ID \u304C ${kind}- \u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${id}`,
1429
+ "error",
1430
+ file,
1431
+ "contracts.declarationPrefix",
1432
+ [id]
1433
+ )
1434
+ );
1435
+ }
1436
+ return issues;
1437
+ }
1438
+ function validateDuplicateContractIds(contractIndex) {
1439
+ const issues = [];
1440
+ for (const [id, files] of contractIndex.idToFiles.entries()) {
1441
+ if (files.size <= 1) {
1442
+ continue;
1443
+ }
1444
+ const sortedFiles = Array.from(files).sort((a, b) => a.localeCompare(b));
1445
+ issues.push(
1446
+ issue(
1447
+ "QFAI-CONTRACT-012",
1448
+ `\u5951\u7D04 ID \u304C\u8907\u6570\u30D5\u30A1\u30A4\u30EB\u3067\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${id} (${sortedFiles.join(
1449
+ ", "
1450
+ )})`,
1451
+ "error",
1452
+ sortedFiles[0],
1453
+ "contracts.idDuplicate",
1454
+ [id]
1455
+ )
1456
+ );
1457
+ }
1458
+ return issues;
1459
+ }
1115
1460
  function hasOpenApi(doc) {
1116
1461
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1117
1462
  }
1118
- function formatError3(error) {
1463
+ function formatError4(error) {
1119
1464
  if (error instanceof Error) {
1120
1465
  return error.message;
1121
1466
  }
@@ -1140,279 +1485,98 @@ function issue(code, message, severity, file, rule, refs) {
1140
1485
  }
1141
1486
 
1142
1487
  // src/core/validators/delta.ts
1143
- var import_promises8 = require("fs/promises");
1144
- var import_node_path7 = __toESM(require("path"), 1);
1488
+ var import_promises9 = require("fs/promises");
1489
+ var import_node_path9 = __toESM(require("path"), 1);
1145
1490
  var SECTION_RE = /^##\s+変更区分/m;
1146
1491
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1147
1492
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
1148
1493
  var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
1149
1494
  var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
1150
1495
  async function validateDeltas(root, config) {
1151
- const specsRoot = resolvePath(root, config, "specsDir");
1152
- const packs = await collectSpecPackDirs(specsRoot);
1153
- if (packs.length === 0) {
1154
- return [];
1155
- }
1156
- const issues = [];
1157
- for (const pack of packs) {
1158
- const deltaPath = import_node_path7.default.join(pack, "delta.md");
1159
- let text;
1160
- try {
1161
- text = await (0, import_promises8.readFile)(deltaPath, "utf-8");
1162
- } catch (error) {
1163
- if (isMissingFileError2(error)) {
1164
- issues.push(
1165
- issue2(
1166
- "QFAI-DELTA-001",
1167
- "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1168
- "error",
1169
- deltaPath,
1170
- "delta.exists"
1171
- )
1172
- );
1173
- continue;
1174
- }
1175
- throw error;
1176
- }
1177
- const hasSection = SECTION_RE.test(text);
1178
- const hasCompatibility = COMPAT_LINE_RE.test(text);
1179
- const hasChange = CHANGE_LINE_RE.test(text);
1180
- if (!hasSection || !hasCompatibility || !hasChange) {
1181
- issues.push(
1182
- issue2(
1183
- "QFAI-DELTA-002",
1184
- "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
1185
- "error",
1186
- deltaPath,
1187
- "delta.section"
1188
- )
1189
- );
1190
- continue;
1191
- }
1192
- const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
1193
- const changeChecked = CHANGE_CHECKED_RE.test(text);
1194
- if (compatibilityChecked === changeChecked) {
1195
- issues.push(
1196
- issue2(
1197
- "QFAI-DELTA-003",
1198
- "delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
1199
- "error",
1200
- deltaPath,
1201
- "delta.classification"
1202
- )
1203
- );
1204
- }
1205
- }
1206
- return issues;
1207
- }
1208
- function isMissingFileError2(error) {
1209
- if (!error || typeof error !== "object") {
1210
- return false;
1211
- }
1212
- return error.code === "ENOENT";
1213
- }
1214
- function issue2(code, message, severity, file, rule, refs) {
1215
- const issue7 = {
1216
- code,
1217
- severity,
1218
- message
1219
- };
1220
- if (file) {
1221
- issue7.file = file;
1222
- }
1223
- if (rule) {
1224
- issue7.rule = rule;
1225
- }
1226
- if (refs && refs.length > 0) {
1227
- issue7.refs = refs;
1228
- }
1229
- return issue7;
1230
- }
1231
-
1232
- // src/core/validators/ids.ts
1233
- var import_promises10 = require("fs/promises");
1234
- var import_node_path9 = __toESM(require("path"), 1);
1235
-
1236
- // src/core/contractIndex.ts
1237
- var import_promises9 = require("fs/promises");
1238
- var import_node_path8 = __toESM(require("path"), 1);
1239
- async function buildContractIndex(root, config) {
1240
- const contractsRoot = resolvePath(root, config, "contractsDir");
1241
- const uiRoot = import_node_path8.default.join(contractsRoot, "ui");
1242
- const apiRoot = import_node_path8.default.join(contractsRoot, "api");
1243
- const dataRoot = import_node_path8.default.join(contractsRoot, "db");
1244
- const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1245
- collectUiContractFiles(uiRoot),
1246
- collectApiContractFiles(apiRoot),
1247
- collectDataContractFiles(dataRoot)
1248
- ]);
1249
- const index = {
1250
- ids: /* @__PURE__ */ new Set(),
1251
- idToFiles: /* @__PURE__ */ new Map(),
1252
- files: { ui: uiFiles, api: apiFiles, data: dataFiles },
1253
- structuredParseFailedFiles: /* @__PURE__ */ new Set()
1254
- };
1255
- await indexUiContracts(uiFiles, index);
1256
- await indexApiContracts(apiFiles, index);
1257
- await indexDataContracts(dataFiles, index);
1258
- return index;
1259
- }
1260
- async function indexUiContracts(files, index) {
1261
- for (const file of files) {
1262
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1263
- try {
1264
- const doc = parseStructuredContract(file, text);
1265
- extractUiContractIds(doc).forEach((id) => record(index, id, file));
1266
- } catch {
1267
- index.structuredParseFailedFiles.add(file);
1268
- extractIds(text, "UI").forEach((id) => record(index, id, file));
1269
- }
1270
- }
1271
- }
1272
- async function indexApiContracts(files, index) {
1273
- for (const file of files) {
1274
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1275
- try {
1276
- const doc = parseStructuredContract(file, text);
1277
- extractApiContractIds(doc).forEach((id) => record(index, id, file));
1278
- } catch {
1279
- index.structuredParseFailedFiles.add(file);
1280
- extractIds(text, "API").forEach((id) => record(index, id, file));
1281
- }
1282
- }
1283
- }
1284
- async function indexDataContracts(files, index) {
1285
- for (const file of files) {
1286
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1287
- extractIds(text, "DATA").forEach((id) => record(index, id, file));
1288
- }
1289
- }
1290
- function record(index, id, file) {
1291
- index.ids.add(id);
1292
- const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
1293
- current.add(file);
1294
- index.idToFiles.set(id, current);
1295
- }
1296
-
1297
- // src/core/parse/markdown.ts
1298
- var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1299
- function parseHeadings(md) {
1300
- const lines = md.split(/\r?\n/);
1301
- const headings = [];
1302
- for (let i = 0; i < lines.length; i++) {
1303
- const line = lines[i] ?? "";
1304
- const match = line.match(HEADING_RE);
1305
- if (!match) continue;
1306
- const levelToken = match[1];
1307
- const title = match[2];
1308
- if (!levelToken || !title) continue;
1309
- headings.push({
1310
- level: levelToken.length,
1311
- title: title.trim(),
1312
- line: i + 1
1313
- });
1314
- }
1315
- return headings;
1316
- }
1317
- function extractH2Sections(md) {
1318
- const lines = md.split(/\r?\n/);
1319
- const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1320
- const sections = /* @__PURE__ */ new Map();
1321
- for (let i = 0; i < headings.length; i++) {
1322
- const current = headings[i];
1323
- if (!current) continue;
1324
- const next = headings[i + 1];
1325
- const startLine = current.line + 1;
1326
- const endLine = (next?.line ?? lines.length + 1) - 1;
1327
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1328
- sections.set(current.title.trim(), {
1329
- title: current.title.trim(),
1330
- startLine,
1331
- endLine,
1332
- body
1333
- });
1334
- }
1335
- return sections;
1336
- }
1337
-
1338
- // src/core/parse/spec.ts
1339
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1340
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1341
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1342
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1343
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1344
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1345
- function parseSpec(md, file) {
1346
- const headings = parseHeadings(md);
1347
- const h1 = headings.find((heading) => heading.level === 1);
1348
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1349
- const sections = extractH2Sections(md);
1350
- const sectionNames = new Set(Array.from(sections.keys()));
1351
- const brSection = sections.get(BR_SECTION_TITLE);
1352
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1353
- const startLine = brSection?.startLine ?? 1;
1354
- const brs = [];
1355
- const brsWithoutPriority = [];
1356
- const brsWithInvalidPriority = [];
1357
- for (let i = 0; i < brLines.length; i++) {
1358
- const lineText = brLines[i] ?? "";
1359
- const lineNumber = startLine + i;
1360
- const validMatch = lineText.match(BR_LINE_RE);
1361
- if (validMatch) {
1362
- const id = validMatch[1];
1363
- const priority = validMatch[2];
1364
- const text = validMatch[3];
1365
- if (!id || !priority || !text) continue;
1366
- brs.push({
1367
- id,
1368
- priority,
1369
- text: text.trim(),
1370
- line: lineNumber
1371
- });
1372
- continue;
1373
- }
1374
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1375
- if (anyPriorityMatch) {
1376
- const id = anyPriorityMatch[1];
1377
- const priority = anyPriorityMatch[2];
1378
- const text = anyPriorityMatch[3];
1379
- if (!id || !priority || !text) continue;
1380
- if (!VALID_PRIORITIES.has(priority)) {
1381
- brsWithInvalidPriority.push({
1382
- id,
1383
- priority,
1384
- text: text.trim(),
1385
- line: lineNumber
1386
- });
1496
+ const specsRoot = resolvePath(root, config, "specsDir");
1497
+ const packs = await collectSpecPackDirs(specsRoot);
1498
+ if (packs.length === 0) {
1499
+ return [];
1500
+ }
1501
+ const issues = [];
1502
+ for (const pack of packs) {
1503
+ const deltaPath = import_node_path9.default.join(pack, "delta.md");
1504
+ let text;
1505
+ try {
1506
+ text = await (0, import_promises9.readFile)(deltaPath, "utf-8");
1507
+ } catch (error) {
1508
+ if (isMissingFileError2(error)) {
1509
+ issues.push(
1510
+ issue2(
1511
+ "QFAI-DELTA-001",
1512
+ "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1513
+ "error",
1514
+ deltaPath,
1515
+ "delta.exists"
1516
+ )
1517
+ );
1518
+ continue;
1387
1519
  }
1520
+ throw error;
1521
+ }
1522
+ const hasSection = SECTION_RE.test(text);
1523
+ const hasCompatibility = COMPAT_LINE_RE.test(text);
1524
+ const hasChange = CHANGE_LINE_RE.test(text);
1525
+ if (!hasSection || !hasCompatibility || !hasChange) {
1526
+ issues.push(
1527
+ issue2(
1528
+ "QFAI-DELTA-002",
1529
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
1530
+ "error",
1531
+ deltaPath,
1532
+ "delta.section"
1533
+ )
1534
+ );
1388
1535
  continue;
1389
1536
  }
1390
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1391
- if (noPriorityMatch) {
1392
- const id = noPriorityMatch[1];
1393
- const text = noPriorityMatch[2];
1394
- if (!id || !text) continue;
1395
- brsWithoutPriority.push({
1396
- id,
1397
- text: text.trim(),
1398
- line: lineNumber
1399
- });
1537
+ const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
1538
+ const changeChecked = CHANGE_CHECKED_RE.test(text);
1539
+ if (compatibilityChecked === changeChecked) {
1540
+ issues.push(
1541
+ issue2(
1542
+ "QFAI-DELTA-003",
1543
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
1544
+ "error",
1545
+ deltaPath,
1546
+ "delta.classification"
1547
+ )
1548
+ );
1400
1549
  }
1401
1550
  }
1402
- const parsed = {
1403
- file,
1404
- sections: sectionNames,
1405
- brs,
1406
- brsWithoutPriority,
1407
- brsWithInvalidPriority
1551
+ return issues;
1552
+ }
1553
+ function isMissingFileError2(error) {
1554
+ if (!error || typeof error !== "object") {
1555
+ return false;
1556
+ }
1557
+ return error.code === "ENOENT";
1558
+ }
1559
+ function issue2(code, message, severity, file, rule, refs) {
1560
+ const issue7 = {
1561
+ code,
1562
+ severity,
1563
+ message
1408
1564
  };
1409
- if (specId) {
1410
- parsed.specId = specId;
1565
+ if (file) {
1566
+ issue7.file = file;
1411
1567
  }
1412
- return parsed;
1568
+ if (rule) {
1569
+ issue7.rule = rule;
1570
+ }
1571
+ if (refs && refs.length > 0) {
1572
+ issue7.refs = refs;
1573
+ }
1574
+ return issue7;
1413
1575
  }
1414
1576
 
1415
1577
  // src/core/validators/ids.ts
1578
+ var import_promises10 = require("fs/promises");
1579
+ var import_node_path10 = __toESM(require("path"), 1);
1416
1580
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1417
1581
  async function validateDefinedIds(root, config) {
1418
1582
  const issues = [];
@@ -1478,7 +1642,7 @@ function recordId(out, id, file) {
1478
1642
  }
1479
1643
  function formatFileList(files, root) {
1480
1644
  return files.map((file) => {
1481
- const relative = import_node_path9.default.relative(root, file);
1645
+ const relative = import_node_path10.default.relative(root, file);
1482
1646
  return relative.length > 0 ? relative : file;
1483
1647
  }).join(", ");
1484
1648
  }
@@ -1507,7 +1671,6 @@ var WHEN_PATTERN = /\bWhen\b/;
1507
1671
  var THEN_PATTERN = /\bThen\b/;
1508
1672
  var SC_TAG_RE4 = /^SC-\d{4}$/;
1509
1673
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1510
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1511
1674
  async function validateScenarios(root, config) {
1512
1675
  const specsRoot = resolvePath(root, config, "specsDir");
1513
1676
  const entries = await collectSpecEntries(specsRoot);
@@ -1556,7 +1719,7 @@ function validateScenarioContent(text, file) {
1556
1719
  "SC",
1557
1720
  "UI",
1558
1721
  "API",
1559
- "DATA",
1722
+ "DB",
1560
1723
  "ADR"
1561
1724
  ]);
1562
1725
  if (invalidIds.length > 0) {
@@ -1587,17 +1750,7 @@ function validateScenarioContent(text, file) {
1587
1750
  const featureSpecTags = document.featureTags.filter(
1588
1751
  (tag) => SPEC_TAG_RE2.test(tag)
1589
1752
  );
1590
- if (featureSpecTags.length === 0) {
1591
- issues.push(
1592
- issue4(
1593
- "QFAI-SC-009",
1594
- "Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1595
- "error",
1596
- file,
1597
- "scenario.featureSpec"
1598
- )
1599
- );
1600
- } else if (featureSpecTags.length > 1) {
1753
+ if (featureSpecTags.length > 1) {
1601
1754
  issues.push(
1602
1755
  issue4(
1603
1756
  "QFAI-SC-009",
@@ -1625,17 +1778,6 @@ function validateScenarioContent(text, file) {
1625
1778
  )
1626
1779
  );
1627
1780
  }
1628
- if (document.scenarios.length > 1) {
1629
- issues.push(
1630
- issue4(
1631
- "QFAI-SC-011",
1632
- `Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
1633
- "error",
1634
- file,
1635
- "scenario.single"
1636
- )
1637
- );
1638
- }
1639
1781
  for (const scenario of document.scenarios) {
1640
1782
  if (scenario.tags.length === 0) {
1641
1783
  issues.push(
@@ -1656,12 +1798,6 @@ function validateScenarioContent(text, file) {
1656
1798
  } else if (scTags.length > 1) {
1657
1799
  missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1658
1800
  }
1659
- if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1660
- missingTags.push("SPEC");
1661
- }
1662
- if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1663
- missingTags.push("BR");
1664
- }
1665
1801
  if (missingTags.length > 0) {
1666
1802
  issues.push(
1667
1803
  issue4(
@@ -1781,7 +1917,7 @@ function validateSpecContent(text, file, requiredSections) {
1781
1917
  "SC",
1782
1918
  "UI",
1783
1919
  "API",
1784
- "DATA",
1920
+ "DB",
1785
1921
  "ADR"
1786
1922
  ]);
1787
1923
  if (invalidIds.length > 0) {
@@ -1896,9 +2032,8 @@ function isMissingFileError4(error) {
1896
2032
 
1897
2033
  // src/core/validators/traceability.ts
1898
2034
  var import_promises13 = require("fs/promises");
1899
- var SC_TAG_RE5 = /^SC-\d{4}$/;
1900
2035
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1901
- var BR_TAG_RE3 = /^BR-\d{4}$/;
2036
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
1902
2037
  async function validateTraceability(root, config) {
1903
2038
  const issues = [];
1904
2039
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1911,8 +2046,7 @@ async function validateTraceability(root, config) {
1911
2046
  const brIdsInSpecs = /* @__PURE__ */ new Set();
1912
2047
  const brIdsInScenarios = /* @__PURE__ */ new Set();
1913
2048
  const scIdsInScenarios = /* @__PURE__ */ new Set();
1914
- const scenarioContractIds = /* @__PURE__ */ new Set();
1915
- const scWithContracts = /* @__PURE__ */ new Set();
2049
+ const specContractIds = /* @__PURE__ */ new Set();
1916
2050
  const specToBrIds = /* @__PURE__ */ new Map();
1917
2051
  const contractIndex = await buildContractIndex(root, config);
1918
2052
  const contractIds = contractIndex.ids;
@@ -1925,19 +2059,60 @@ async function validateTraceability(root, config) {
1925
2059
  }
1926
2060
  const brIds = parsed.brs.map((br) => br.id);
1927
2061
  brIds.forEach((id) => brIdsInSpecs.add(id));
1928
- const referencedContractIds = /* @__PURE__ */ new Set([
1929
- ...extractIds(text, "UI"),
1930
- ...extractIds(text, "API"),
1931
- ...extractIds(text, "DATA")
1932
- ]);
1933
- const unknownContractIds = Array.from(referencedContractIds).filter(
2062
+ if (parsed.specId) {
2063
+ const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
2064
+ brIds.forEach((id) => current.add(id));
2065
+ specToBrIds.set(parsed.specId, current);
2066
+ }
2067
+ const contractRefs = parsed.contractRefs;
2068
+ if (contractRefs.lines.length === 0) {
2069
+ issues.push(
2070
+ issue6(
2071
+ "QFAI-TRACE-020",
2072
+ "Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2073
+ "error",
2074
+ file,
2075
+ "traceability.specContractRefRequired"
2076
+ )
2077
+ );
2078
+ } else {
2079
+ if (contractRefs.hasNone && contractRefs.ids.length > 0) {
2080
+ issues.push(
2081
+ issue6(
2082
+ "QFAI-TRACE-021",
2083
+ "Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2084
+ "error",
2085
+ file,
2086
+ "traceability.specContractRefFormat"
2087
+ )
2088
+ );
2089
+ }
2090
+ if (contractRefs.invalidTokens.length > 0) {
2091
+ issues.push(
2092
+ issue6(
2093
+ "QFAI-TRACE-021",
2094
+ `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
2095
+ ", "
2096
+ )}`,
2097
+ "error",
2098
+ file,
2099
+ "traceability.specContractRefFormat",
2100
+ contractRefs.invalidTokens
2101
+ )
2102
+ );
2103
+ }
2104
+ }
2105
+ contractRefs.ids.forEach((id) => {
2106
+ specContractIds.add(id);
2107
+ });
2108
+ const unknownContractIds = contractRefs.ids.filter(
1934
2109
  (id) => !contractIds.has(id)
1935
2110
  );
1936
2111
  if (unknownContractIds.length > 0) {
1937
2112
  issues.push(
1938
2113
  issue6(
1939
- "QFAI-TRACE-009",
1940
- `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2114
+ "QFAI-TRACE-021",
2115
+ `Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1941
2116
  ", "
1942
2117
  )}`,
1943
2118
  "error",
@@ -1947,11 +2122,6 @@ async function validateTraceability(root, config) {
1947
2122
  )
1948
2123
  );
1949
2124
  }
1950
- if (parsed.specId) {
1951
- const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
1952
- brIds.forEach((id) => current.add(id));
1953
- specToBrIds.set(parsed.specId, current);
1954
- }
1955
2125
  }
1956
2126
  for (const file of scenarioFiles) {
1957
2127
  const text = await (0, import_promises13.readFile)(file, "utf-8");
@@ -1961,20 +2131,42 @@ async function validateTraceability(root, config) {
1961
2131
  continue;
1962
2132
  }
1963
2133
  const atoms = buildScenarioAtoms(document);
2134
+ const scIdsInFile = /* @__PURE__ */ new Set();
1964
2135
  for (const [index, scenario] of document.scenarios.entries()) {
1965
2136
  const atom = atoms[index];
1966
2137
  if (!atom) {
1967
2138
  continue;
1968
2139
  }
1969
2140
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
1970
- const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
1971
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.test(tag));
1972
- brTags.forEach((id) => brIdsInScenarios.add(id));
1973
- scTags.forEach((id) => scIdsInScenarios.add(id));
1974
- atom.contractIds.forEach((id) => scenarioContractIds.add(id));
1975
- if (atom.contractIds.length > 0) {
1976
- scTags.forEach((id) => scWithContracts.add(id));
2141
+ const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
2142
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
2143
+ if (specTags.length === 0) {
2144
+ issues.push(
2145
+ issue6(
2146
+ "QFAI-TRACE-014",
2147
+ `Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2148
+ "error",
2149
+ file,
2150
+ "traceability.scenarioSpecRequired"
2151
+ )
2152
+ );
2153
+ }
2154
+ if (brTags.length === 0) {
2155
+ issues.push(
2156
+ issue6(
2157
+ "QFAI-TRACE-015",
2158
+ `Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2159
+ "error",
2160
+ file,
2161
+ "traceability.scenarioBrRequired"
2162
+ )
2163
+ );
1977
2164
  }
2165
+ brTags.forEach((id) => brIdsInScenarios.add(id));
2166
+ scTags.forEach((id) => {
2167
+ scIdsInScenarios.add(id);
2168
+ scIdsInFile.add(id);
2169
+ });
1978
2170
  const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
1979
2171
  if (unknownSpecIds.length > 0) {
1980
2172
  issues.push(
@@ -2048,6 +2240,22 @@ async function validateTraceability(root, config) {
2048
2240
  }
2049
2241
  }
2050
2242
  }
2243
+ if (scIdsInFile.size !== 1) {
2244
+ const invalidScIds = Array.from(scIdsInFile).sort(
2245
+ (a, b) => a.localeCompare(b)
2246
+ );
2247
+ const detail = invalidScIds.length === 0 ? "SC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" : `\u8907\u6570\u306E SC \u304C\u5B58\u5728\u3057\u307E\u3059: ${invalidScIds.join(", ")}`;
2248
+ issues.push(
2249
+ issue6(
2250
+ "QFAI-TRACE-012",
2251
+ `Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
2252
+ "error",
2253
+ file,
2254
+ "traceability.specScOneToOne",
2255
+ invalidScIds
2256
+ )
2257
+ );
2258
+ }
2051
2259
  }
2052
2260
  if (upstreamIds.size === 0) {
2053
2261
  return [
@@ -2077,40 +2285,62 @@ async function validateTraceability(root, config) {
2077
2285
  );
2078
2286
  }
2079
2287
  }
2080
- if (config.validation.traceability.scMustTouchContracts && scIdsInScenarios.size > 0) {
2081
- const scWithoutContracts = Array.from(scIdsInScenarios).filter(
2082
- (id) => !scWithContracts.has(id)
2288
+ const scRefsResult = await collectScTestReferences(
2289
+ root,
2290
+ config.validation.traceability.testFileGlobs,
2291
+ config.validation.traceability.testFileExcludeGlobs
2292
+ );
2293
+ const scTestRefs = scRefsResult.refs;
2294
+ const testFileScan = scRefsResult.scan;
2295
+ const hasScenarios = scIdsInScenarios.size > 0;
2296
+ const hasGlobConfig = testFileScan.globs.length > 0;
2297
+ const hasMatchedTests = testFileScan.matchedFileCount > 0;
2298
+ if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
2299
+ const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
2300
+ issues.push(
2301
+ issue6(
2302
+ "QFAI-TRACE-013",
2303
+ `\u30C6\u30B9\u30C8\u63A2\u7D22 glob \u304C\u672A\u8A2D\u5B9A/\u4E0D\u6B63/\u4E00\u81F4\u30D5\u30A1\u30A4\u30EB0\u306E\u305F\u3081 SC\u2192Test \u3092\u5224\u5B9A\u3067\u304D\u307E\u305B\u3093\u3002${detail}`,
2304
+ "error",
2305
+ testsRoot,
2306
+ "traceability.testFileGlobs"
2307
+ )
2308
+ );
2309
+ } else {
2310
+ if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2311
+ const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2312
+ const refs = scTestRefs.get(id);
2313
+ return !refs || refs.size === 0;
2314
+ });
2315
+ if (scWithoutTests.length > 0) {
2316
+ issues.push(
2317
+ issue6(
2318
+ "QFAI-TRACE-010",
2319
+ `SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
2320
+ ", "
2321
+ )}\u3002testFileGlobs \u306B\u4E00\u81F4\u3059\u308B\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u3078 QFAI:SC-xxxx \u3092\u8A18\u8F09\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
2322
+ config.validation.traceability.scNoTestSeverity,
2323
+ testsRoot,
2324
+ "traceability.scMustHaveTest",
2325
+ scWithoutTests
2326
+ )
2327
+ );
2328
+ }
2329
+ }
2330
+ const unknownScIds = Array.from(scTestRefs.keys()).filter(
2331
+ (id) => !scIdsInScenarios.has(id)
2083
2332
  );
2084
- if (scWithoutContracts.length > 0) {
2333
+ if (unknownScIds.length > 0) {
2085
2334
  issues.push(
2086
2335
  issue6(
2087
- "QFAI_TRACE_SC_NO_CONTRACT",
2088
- `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
2336
+ "QFAI-TRACE-011",
2337
+ `\u30C6\u30B9\u30C8\u304C\u672A\u77E5\u306E SC \u3092\u30A2\u30CE\u30C6\u30FC\u30B7\u30E7\u30F3\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownScIds.join(
2089
2338
  ", "
2090
2339
  )}`,
2091
2340
  "error",
2092
- specsRoot,
2093
- "traceability.scMustTouchContracts",
2094
- scWithoutContracts
2095
- )
2096
- );
2097
- }
2098
- }
2099
- if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2100
- const scTestRefs = await collectScTestReferences(testsRoot);
2101
- const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2102
- const refs = scTestRefs.get(id);
2103
- return !refs || refs.size === 0;
2104
- });
2105
- if (scWithoutTests.length > 0) {
2106
- issues.push(
2107
- issue6(
2108
- "QFAI-TRACE-010",
2109
- `SC \u304C tests \u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(", ")}\u3002tests/ \u914D\u4E0B\u306E\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\uFF08.ts/.tsx/.js/.jsx\uFF09\u306B SC ID \u3092\u30B3\u30E1\u30F3\u30C8\u307E\u305F\u306F\u30B3\u30FC\u30C9\u3067\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
2110
- config.validation.traceability.scNoTestSeverity,
2111
2341
  testsRoot,
2112
- "traceability.scMustHaveTest",
2113
- scWithoutTests
2342
+ "traceability.scUnknownInTests",
2343
+ unknownScIds
2114
2344
  )
2115
2345
  );
2116
2346
  }
@@ -2118,16 +2348,16 @@ async function validateTraceability(root, config) {
2118
2348
  if (!config.validation.traceability.allowOrphanContracts) {
2119
2349
  if (contractIds.size > 0) {
2120
2350
  const orphanContracts = Array.from(contractIds).filter(
2121
- (id) => !scenarioContractIds.has(id)
2351
+ (id) => !specContractIds.has(id)
2122
2352
  );
2123
2353
  if (orphanContracts.length > 0) {
2124
2354
  issues.push(
2125
2355
  issue6(
2126
- "QFAI_CONTRACT_ORPHAN",
2127
- `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2356
+ "QFAI-TRACE-022",
2357
+ `\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2128
2358
  "error",
2129
2359
  specsRoot,
2130
- "traceability.allowOrphanContracts",
2360
+ "traceability.contractCoverage",
2131
2361
  orphanContracts
2132
2362
  )
2133
2363
  );
@@ -2173,8 +2403,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2173
2403
  issues.push(
2174
2404
  issue6(
2175
2405
  "QFAI-TRACE-002",
2176
- "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
2177
- "warning",
2406
+ "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\uFF08\u53C2\u8003\u60C5\u5831\uFF09\u3002",
2407
+ "info",
2178
2408
  srcRoot,
2179
2409
  "traceability.codeReferences"
2180
2410
  )
@@ -2217,11 +2447,24 @@ async function validateProject(root, configResult) {
2217
2447
  ...await validateDefinedIds(root, config),
2218
2448
  ...await validateTraceability(root, config)
2219
2449
  ];
2450
+ const specsRoot = resolvePath(root, config, "specsDir");
2451
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
2452
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2453
+ const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
2454
+ root,
2455
+ config.validation.traceability.testFileGlobs,
2456
+ config.validation.traceability.testFileExcludeGlobs
2457
+ );
2458
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
2220
2459
  const toolVersion = await resolveToolVersion();
2221
2460
  return {
2222
2461
  toolVersion,
2223
2462
  issues,
2224
- counts: countIssues(issues)
2463
+ counts: countIssues(issues),
2464
+ traceability: {
2465
+ sc: scCoverage,
2466
+ testFiles
2467
+ }
2225
2468
  };
2226
2469
  }
2227
2470
  function countIssues(issues) {
@@ -2235,16 +2478,16 @@ function countIssues(issues) {
2235
2478
  }
2236
2479
 
2237
2480
  // src/core/report.ts
2238
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
2481
+ var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2239
2482
  async function createReportData(root, validation, configResult) {
2240
2483
  const resolved = configResult ?? await loadConfig(root);
2241
2484
  const config = resolved.config;
2242
2485
  const configPath = resolved.configPath;
2243
2486
  const specsRoot = resolvePath(root, config, "specsDir");
2244
2487
  const contractsRoot = resolvePath(root, config, "contractsDir");
2245
- const apiRoot = import_node_path10.default.join(contractsRoot, "api");
2246
- const uiRoot = import_node_path10.default.join(contractsRoot, "ui");
2247
- const dbRoot = import_node_path10.default.join(contractsRoot, "db");
2488
+ const apiRoot = import_node_path11.default.join(contractsRoot, "api");
2489
+ const uiRoot = import_node_path11.default.join(contractsRoot, "ui");
2490
+ const dbRoot = import_node_path11.default.join(contractsRoot, "db");
2248
2491
  const srcRoot = resolvePath(root, config, "srcDir");
2249
2492
  const testsRoot = resolvePath(root, config, "testsDir");
2250
2493
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2254,6 +2497,23 @@ async function createReportData(root, validation, configResult) {
2254
2497
  ui: uiFiles,
2255
2498
  db: dbFiles
2256
2499
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
2500
+ const contractIndex = await buildContractIndex(root, config);
2501
+ const specContractRefs = await collectSpecContractRefs(specFiles);
2502
+ const contractIdList = Array.from(contractIndex.ids);
2503
+ const referencedContracts = /* @__PURE__ */ new Set();
2504
+ for (const ids of specContractRefs.specToContractIds.values()) {
2505
+ ids.forEach((id) => referencedContracts.add(id));
2506
+ }
2507
+ const referencedContractCount = contractIdList.filter(
2508
+ (id) => referencedContracts.has(id)
2509
+ ).length;
2510
+ const orphanContractCount = contractIdList.filter(
2511
+ (id) => !referencedContracts.has(id)
2512
+ ).length;
2513
+ const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
2514
+ const specToContractIdsRecord = mapToSortedRecord(
2515
+ specContractRefs.specToContractIds
2516
+ );
2257
2517
  const idsByPrefix = await collectIds([
2258
2518
  ...specFiles,
2259
2519
  ...scenarioFiles,
@@ -2271,8 +2531,15 @@ async function createReportData(root, validation, configResult) {
2271
2531
  testsRoot
2272
2532
  );
2273
2533
  const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2274
- const scTestRefs = await collectScTestReferences(testsRoot);
2275
- const scCoverage = buildScCoverage(scIds, scTestRefs);
2534
+ const scRefsResult = await collectScTestReferences(
2535
+ root,
2536
+ config.validation.traceability.testFileGlobs,
2537
+ config.validation.traceability.testFileExcludeGlobs
2538
+ );
2539
+ const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2540
+ const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2541
+ const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2542
+ const scSourceRecord = mapToSortedRecord(scSources);
2276
2543
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2277
2544
  const version = await resolveToolVersion();
2278
2545
  return {
@@ -2297,12 +2564,24 @@ async function createReportData(root, validation, configResult) {
2297
2564
  sc: idsByPrefix.SC,
2298
2565
  ui: idsByPrefix.UI,
2299
2566
  api: idsByPrefix.API,
2300
- data: idsByPrefix.DATA
2567
+ db: idsByPrefix.DB
2301
2568
  },
2302
2569
  traceability: {
2303
2570
  upstreamIdsFound: upstreamIds.size,
2304
2571
  referencedInCodeOrTests: traceability,
2305
- sc: scCoverage
2572
+ sc: scCoverage,
2573
+ scSources: scSourceRecord,
2574
+ testFiles,
2575
+ contracts: {
2576
+ total: contractIdList.length,
2577
+ referenced: referencedContractCount,
2578
+ orphan: orphanContractCount,
2579
+ idToSpecs: contractIdToSpecsRecord
2580
+ },
2581
+ specs: {
2582
+ contractRefMissing: specContractRefs.missingRefSpecs.size,
2583
+ specToContractIds: specToContractIdsRecord
2584
+ }
2306
2585
  },
2307
2586
  issues: resolvedValidation.issues
2308
2587
  };
@@ -2331,7 +2610,7 @@ function formatReportMarkdown(data) {
2331
2610
  lines.push(formatIdLine("SC", data.ids.sc));
2332
2611
  lines.push(formatIdLine("UI", data.ids.ui));
2333
2612
  lines.push(formatIdLine("API", data.ids.api));
2334
- lines.push(formatIdLine("DATA", data.ids.data));
2613
+ lines.push(formatIdLine("DB", data.ids.db));
2335
2614
  lines.push("");
2336
2615
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
2337
2616
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
@@ -2339,14 +2618,77 @@ function formatReportMarkdown(data) {
2339
2618
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2340
2619
  );
2341
2620
  lines.push("");
2621
+ lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
2622
+ lines.push(`- total: ${data.traceability.contracts.total}`);
2623
+ lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
2624
+ lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
2625
+ lines.push(
2626
+ `- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
2627
+ );
2628
+ lines.push("");
2629
+ lines.push("## \u5951\u7D04\u2192Spec");
2630
+ const contractToSpecs = data.traceability.contracts.idToSpecs;
2631
+ const contractIds = Object.keys(contractToSpecs).sort(
2632
+ (a, b) => a.localeCompare(b)
2633
+ );
2634
+ if (contractIds.length === 0) {
2635
+ lines.push("- (none)");
2636
+ } else {
2637
+ for (const contractId of contractIds) {
2638
+ const specs = contractToSpecs[contractId] ?? [];
2639
+ if (specs.length === 0) {
2640
+ lines.push(`- ${contractId}: (none)`);
2641
+ } else {
2642
+ lines.push(`- ${contractId}: ${specs.join(", ")}`);
2643
+ }
2644
+ }
2645
+ }
2646
+ lines.push("");
2647
+ lines.push("## Spec\u2192\u5951\u7D04");
2648
+ const specToContracts = data.traceability.specs.specToContractIds;
2649
+ const specIds = Object.keys(specToContracts).sort(
2650
+ (a, b) => a.localeCompare(b)
2651
+ );
2652
+ if (specIds.length === 0) {
2653
+ lines.push("- (none)");
2654
+ } else {
2655
+ for (const specId of specIds) {
2656
+ const contractIds2 = specToContracts[specId] ?? [];
2657
+ if (contractIds2.length === 0) {
2658
+ lines.push(`- ${specId}: (none)`);
2659
+ } else {
2660
+ lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
2661
+ }
2662
+ }
2663
+ }
2664
+ lines.push("");
2342
2665
  lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2343
2666
  lines.push(`- total: ${data.traceability.sc.total}`);
2344
2667
  lines.push(`- covered: ${data.traceability.sc.covered}`);
2345
2668
  lines.push(`- missing: ${data.traceability.sc.missing}`);
2669
+ lines.push(
2670
+ `- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
2671
+ );
2672
+ lines.push(
2673
+ `- testFileExcludeGlobs: ${formatList(
2674
+ data.traceability.testFiles.excludeGlobs
2675
+ )}`
2676
+ );
2677
+ lines.push(
2678
+ `- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
2679
+ );
2346
2680
  if (data.traceability.sc.missingIds.length === 0) {
2347
2681
  lines.push("- missingIds: (none)");
2348
2682
  } else {
2349
- lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
2683
+ const sources = data.traceability.scSources;
2684
+ const missingWithSources = data.traceability.sc.missingIds.map((id) => {
2685
+ const files = sources[id] ?? [];
2686
+ if (files.length === 0) {
2687
+ return id;
2688
+ }
2689
+ return `${id} (${files.join(", ")})`;
2690
+ });
2691
+ lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
2350
2692
  }
2351
2693
  lines.push("");
2352
2694
  lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
@@ -2365,6 +2707,20 @@ function formatReportMarkdown(data) {
2365
2707
  }
2366
2708
  }
2367
2709
  lines.push("");
2710
+ lines.push("## Spec:SC=1:1 \u9055\u53CD");
2711
+ const specScIssues = data.issues.filter(
2712
+ (item) => item.code === "QFAI-TRACE-012"
2713
+ );
2714
+ if (specScIssues.length === 0) {
2715
+ lines.push("- (none)");
2716
+ } else {
2717
+ for (const item of specScIssues) {
2718
+ const location = item.file ?? "(unknown)";
2719
+ const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
2720
+ lines.push(`- ${location}: ${refs}`);
2721
+ }
2722
+ }
2723
+ lines.push("");
2368
2724
  lines.push("## Hotspots");
2369
2725
  const hotspots = buildHotspots(data.issues);
2370
2726
  if (hotspots.length === 0) {
@@ -2379,7 +2735,7 @@ function formatReportMarkdown(data) {
2379
2735
  lines.push("");
2380
2736
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
2381
2737
  const traceIssues = data.issues.filter(
2382
- (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-") || item.code === "QFAI_CONTRACT_ORPHAN"
2738
+ (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
2383
2739
  );
2384
2740
  if (traceIssues.length === 0) {
2385
2741
  lines.push("- (none)");
@@ -2409,6 +2765,33 @@ function formatReportMarkdown(data) {
2409
2765
  function formatReportJson(data) {
2410
2766
  return JSON.stringify(data, null, 2);
2411
2767
  }
2768
+ async function collectSpecContractRefs(specFiles) {
2769
+ const specToContractIds = /* @__PURE__ */ new Map();
2770
+ const idToSpecs = /* @__PURE__ */ new Map();
2771
+ const missingRefSpecs = /* @__PURE__ */ new Set();
2772
+ for (const file of specFiles) {
2773
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
2774
+ const parsed = parseSpec(text, file);
2775
+ const specKey = parsed.specId ?? file;
2776
+ const refs = parsed.contractRefs;
2777
+ if (refs.lines.length === 0) {
2778
+ missingRefSpecs.add(specKey);
2779
+ }
2780
+ const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
2781
+ for (const id of refs.ids) {
2782
+ currentContracts.add(id);
2783
+ const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
2784
+ specs.add(specKey);
2785
+ idToSpecs.set(id, specs);
2786
+ }
2787
+ specToContractIds.set(specKey, currentContracts);
2788
+ }
2789
+ return {
2790
+ specToContractIds,
2791
+ idToSpecs,
2792
+ missingRefSpecs
2793
+ };
2794
+ }
2412
2795
  async function collectIds(files) {
2413
2796
  const result = {
2414
2797
  SPEC: /* @__PURE__ */ new Set(),
@@ -2416,7 +2799,7 @@ async function collectIds(files) {
2416
2799
  SC: /* @__PURE__ */ new Set(),
2417
2800
  UI: /* @__PURE__ */ new Set(),
2418
2801
  API: /* @__PURE__ */ new Set(),
2419
- DATA: /* @__PURE__ */ new Set()
2802
+ DB: /* @__PURE__ */ new Set()
2420
2803
  };
2421
2804
  for (const file of files) {
2422
2805
  const text = await (0, import_promises14.readFile)(file, "utf-8");
@@ -2431,7 +2814,7 @@ async function collectIds(files) {
2431
2814
  SC: toSortedArray2(result.SC),
2432
2815
  UI: toSortedArray2(result.UI),
2433
2816
  API: toSortedArray2(result.API),
2434
- DATA: toSortedArray2(result.DATA)
2817
+ DB: toSortedArray2(result.DB)
2435
2818
  };
2436
2819
  }
2437
2820
  async function collectUpstreamIds(files) {
@@ -2475,9 +2858,22 @@ function formatIdLine(label, values) {
2475
2858
  }
2476
2859
  return `- ${label}: ${values.join(", ")}`;
2477
2860
  }
2861
+ function formatList(values) {
2862
+ if (values.length === 0) {
2863
+ return "(none)";
2864
+ }
2865
+ return values.join(", ");
2866
+ }
2478
2867
  function toSortedArray2(values) {
2479
2868
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2480
2869
  }
2870
+ function mapToSortedRecord(values) {
2871
+ const record2 = {};
2872
+ for (const [key, files] of values.entries()) {
2873
+ record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
2874
+ }
2875
+ return record2;
2876
+ }
2481
2877
  function buildHotspots(issues) {
2482
2878
  const map = /* @__PURE__ */ new Map();
2483
2879
  for (const issue7 of issues) {