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.mjs CHANGED
@@ -27,8 +27,9 @@ var defaultConfig = {
27
27
  },
28
28
  traceability: {
29
29
  brMustHaveSc: true,
30
- scMustTouchContracts: true,
31
30
  scMustHaveTest: true,
31
+ testFileGlobs: [],
32
+ testFileExcludeGlobs: [],
32
33
  scNoTestSeverity: "error",
33
34
  allowOrphanContracts: false,
34
35
  unknownContractIdSeverity: "error"
@@ -202,13 +203,6 @@ function normalizeValidation(raw, configPath, issues) {
202
203
  configPath,
203
204
  issues
204
205
  ),
205
- scMustTouchContracts: readBoolean(
206
- traceabilityRaw?.scMustTouchContracts,
207
- base.traceability.scMustTouchContracts,
208
- "validation.traceability.scMustTouchContracts",
209
- configPath,
210
- issues
211
- ),
212
206
  scMustHaveTest: readBoolean(
213
207
  traceabilityRaw?.scMustHaveTest,
214
208
  base.traceability.scMustHaveTest,
@@ -216,6 +210,20 @@ function normalizeValidation(raw, configPath, issues) {
216
210
  configPath,
217
211
  issues
218
212
  ),
213
+ testFileGlobs: readStringArray(
214
+ traceabilityRaw?.testFileGlobs,
215
+ base.traceability.testFileGlobs,
216
+ "validation.traceability.testFileGlobs",
217
+ configPath,
218
+ issues
219
+ ),
220
+ testFileExcludeGlobs: readStringArray(
221
+ traceabilityRaw?.testFileExcludeGlobs,
222
+ base.traceability.testFileExcludeGlobs,
223
+ "validation.traceability.testFileExcludeGlobs",
224
+ configPath,
225
+ issues
226
+ ),
219
227
  scNoTestSeverity: readTraceabilitySeverity(
220
228
  traceabilityRaw?.scNoTestSeverity,
221
229
  base.traceability.scNoTestSeverity,
@@ -348,14 +356,14 @@ function isRecord(value) {
348
356
  }
349
357
 
350
358
  // src/core/ids.ts
351
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
359
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
352
360
  var STRICT_ID_PATTERNS = {
353
361
  SPEC: /\bSPEC-\d{4}\b/g,
354
362
  BR: /\bBR-\d{4}\b/g,
355
363
  SC: /\bSC-\d{4}\b/g,
356
364
  UI: /\bUI-\d{4}\b/g,
357
365
  API: /\bAPI-\d{4}\b/g,
358
- DATA: /\bDATA-\d{4}\b/g,
366
+ DB: /\bDB-\d{4}\b/g,
359
367
  ADR: /\bADR-\d{4}\b/g
360
368
  };
361
369
  var LOOSE_ID_PATTERNS = {
@@ -364,7 +372,7 @@ var LOOSE_ID_PATTERNS = {
364
372
  SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
365
373
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
366
374
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
367
- DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
375
+ DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
368
376
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
369
377
  };
370
378
  function extractIds(text, prefix) {
@@ -402,7 +410,11 @@ function isValidId(value, prefix) {
402
410
 
403
411
  // src/core/report.ts
404
412
  import { readFile as readFile11 } from "fs/promises";
405
- import path10 from "path";
413
+ import path11 from "path";
414
+
415
+ // src/core/contractIndex.ts
416
+ import { readFile as readFile2 } from "fs/promises";
417
+ import path4 from "path";
406
418
 
407
419
  // src/core/discovery.ts
408
420
  import { access as access2 } from "fs/promises";
@@ -410,6 +422,7 @@ import { access as access2 } from "fs/promises";
410
422
  // src/core/fs.ts
411
423
  import { access, readdir } from "fs/promises";
412
424
  import path2 from "path";
425
+ import fg from "fast-glob";
413
426
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
414
427
  "node_modules",
415
428
  ".git",
@@ -431,6 +444,18 @@ async function collectFiles(root, options = {}) {
431
444
  await walk(root, root, ignoreDirs, extensions, entries);
432
445
  return entries;
433
446
  }
447
+ async function collectFilesByGlobs(root, options) {
448
+ if (options.globs.length === 0) {
449
+ return [];
450
+ }
451
+ return fg(options.globs, {
452
+ cwd: root,
453
+ ignore: options.ignore ?? [],
454
+ onlyFiles: true,
455
+ absolute: true,
456
+ unique: true
457
+ });
458
+ }
434
459
  async function walk(base, current, ignoreDirs, extensions, out) {
435
460
  const items = await readdir(current, { withFileTypes: true });
436
461
  for (const item of items) {
@@ -542,8 +567,221 @@ async function exists2(target) {
542
567
  }
543
568
  }
544
569
 
570
+ // src/core/contractsDecl.ts
571
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
572
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
573
+ function extractDeclaredContractIds(text) {
574
+ const ids = [];
575
+ for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
576
+ const id = match[1];
577
+ if (id) {
578
+ ids.push(id);
579
+ }
580
+ }
581
+ return ids;
582
+ }
583
+ function stripContractDeclarationLines(text) {
584
+ return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
585
+ }
586
+
587
+ // src/core/contractIndex.ts
588
+ async function buildContractIndex(root, config) {
589
+ const contractsRoot = resolvePath(root, config, "contractsDir");
590
+ const uiRoot = path4.join(contractsRoot, "ui");
591
+ const apiRoot = path4.join(contractsRoot, "api");
592
+ const dbRoot = path4.join(contractsRoot, "db");
593
+ const [uiFiles, apiFiles, dbFiles] = await Promise.all([
594
+ collectUiContractFiles(uiRoot),
595
+ collectApiContractFiles(apiRoot),
596
+ collectDataContractFiles(dbRoot)
597
+ ]);
598
+ const index = {
599
+ ids: /* @__PURE__ */ new Set(),
600
+ idToFiles: /* @__PURE__ */ new Map(),
601
+ files: { ui: uiFiles, api: apiFiles, db: dbFiles }
602
+ };
603
+ await indexContractFiles(uiFiles, index);
604
+ await indexContractFiles(apiFiles, index);
605
+ await indexContractFiles(dbFiles, index);
606
+ return index;
607
+ }
608
+ async function indexContractFiles(files, index) {
609
+ for (const file of files) {
610
+ const text = await readFile2(file, "utf-8");
611
+ extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
612
+ }
613
+ }
614
+ function record(index, id, file) {
615
+ index.ids.add(id);
616
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
617
+ current.add(file);
618
+ index.idToFiles.set(id, current);
619
+ }
620
+
621
+ // src/core/parse/markdown.ts
622
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
623
+ function parseHeadings(md) {
624
+ const lines = md.split(/\r?\n/);
625
+ const headings = [];
626
+ for (let i = 0; i < lines.length; i++) {
627
+ const line = lines[i] ?? "";
628
+ const match = line.match(HEADING_RE);
629
+ if (!match) continue;
630
+ const levelToken = match[1];
631
+ const title = match[2];
632
+ if (!levelToken || !title) continue;
633
+ headings.push({
634
+ level: levelToken.length,
635
+ title: title.trim(),
636
+ line: i + 1
637
+ });
638
+ }
639
+ return headings;
640
+ }
641
+ function extractH2Sections(md) {
642
+ const lines = md.split(/\r?\n/);
643
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
644
+ const sections = /* @__PURE__ */ new Map();
645
+ for (let i = 0; i < headings.length; i++) {
646
+ const current = headings[i];
647
+ if (!current) continue;
648
+ const next = headings[i + 1];
649
+ const startLine = current.line + 1;
650
+ const endLine = (next?.line ?? lines.length + 1) - 1;
651
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
652
+ sections.set(current.title.trim(), {
653
+ title: current.title.trim(),
654
+ startLine,
655
+ endLine,
656
+ body
657
+ });
658
+ }
659
+ return sections;
660
+ }
661
+
662
+ // src/core/parse/spec.ts
663
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
664
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
665
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
666
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
667
+ var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
668
+ var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
669
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
670
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
671
+ function parseSpec(md, file) {
672
+ const headings = parseHeadings(md);
673
+ const h1 = headings.find((heading) => heading.level === 1);
674
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
675
+ const sections = extractH2Sections(md);
676
+ const sectionNames = new Set(Array.from(sections.keys()));
677
+ const brSection = sections.get(BR_SECTION_TITLE);
678
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
679
+ const startLine = brSection?.startLine ?? 1;
680
+ const brs = [];
681
+ const brsWithoutPriority = [];
682
+ const brsWithInvalidPriority = [];
683
+ for (let i = 0; i < brLines.length; i++) {
684
+ const lineText = brLines[i] ?? "";
685
+ const lineNumber = startLine + i;
686
+ const validMatch = lineText.match(BR_LINE_RE);
687
+ if (validMatch) {
688
+ const id = validMatch[1];
689
+ const priority = validMatch[2];
690
+ const text = validMatch[3];
691
+ if (!id || !priority || !text) continue;
692
+ brs.push({
693
+ id,
694
+ priority,
695
+ text: text.trim(),
696
+ line: lineNumber
697
+ });
698
+ continue;
699
+ }
700
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
701
+ if (anyPriorityMatch) {
702
+ const id = anyPriorityMatch[1];
703
+ const priority = anyPriorityMatch[2];
704
+ const text = anyPriorityMatch[3];
705
+ if (!id || !priority || !text) continue;
706
+ if (!VALID_PRIORITIES.has(priority)) {
707
+ brsWithInvalidPriority.push({
708
+ id,
709
+ priority,
710
+ text: text.trim(),
711
+ line: lineNumber
712
+ });
713
+ }
714
+ continue;
715
+ }
716
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
717
+ if (noPriorityMatch) {
718
+ const id = noPriorityMatch[1];
719
+ const text = noPriorityMatch[2];
720
+ if (!id || !text) continue;
721
+ brsWithoutPriority.push({
722
+ id,
723
+ text: text.trim(),
724
+ line: lineNumber
725
+ });
726
+ }
727
+ }
728
+ const parsed = {
729
+ file,
730
+ sections: sectionNames,
731
+ brs,
732
+ brsWithoutPriority,
733
+ brsWithInvalidPriority,
734
+ contractRefs: parseContractRefs(md)
735
+ };
736
+ if (specId) {
737
+ parsed.specId = specId;
738
+ }
739
+ return parsed;
740
+ }
741
+ function parseContractRefs(md) {
742
+ const lines = [];
743
+ for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
744
+ lines.push((match[1] ?? "").trim());
745
+ }
746
+ const ids = [];
747
+ const invalidTokens = [];
748
+ let hasNone = false;
749
+ for (const line of lines) {
750
+ if (line.length === 0) {
751
+ invalidTokens.push("(empty)");
752
+ continue;
753
+ }
754
+ const tokens = line.split(",").map((token) => token.trim());
755
+ for (const token of tokens) {
756
+ if (token.length === 0) {
757
+ invalidTokens.push("(empty)");
758
+ continue;
759
+ }
760
+ if (token === "none") {
761
+ hasNone = true;
762
+ continue;
763
+ }
764
+ if (CONTRACT_REF_ID_RE.test(token)) {
765
+ ids.push(token);
766
+ continue;
767
+ }
768
+ invalidTokens.push(token);
769
+ }
770
+ }
771
+ return {
772
+ lines,
773
+ ids: unique2(ids),
774
+ invalidTokens: unique2(invalidTokens),
775
+ hasNone
776
+ };
777
+ }
778
+ function unique2(values) {
779
+ return Array.from(new Set(values));
780
+ }
781
+
545
782
  // src/core/traceability.ts
546
- import { readFile as readFile2 } from "fs/promises";
783
+ import { readFile as readFile3 } from "fs/promises";
784
+ import path5 from "path";
547
785
 
548
786
  // src/core/gherkin/parse.ts
549
787
  import {
@@ -580,7 +818,7 @@ var SC_TAG_RE = /^SC-\d{4}$/;
580
818
  var BR_TAG_RE = /^BR-\d{4}$/;
581
819
  var UI_TAG_RE = /^UI-\d{4}$/;
582
820
  var API_TAG_RE = /^API-\d{4}$/;
583
- var DATA_TAG_RE = /^DATA-\d{4}$/;
821
+ var DB_TAG_RE = /^DB-\d{4}$/;
584
822
  function parseScenarioDocument(text, uri) {
585
823
  const { gherkinDocument, errors } = parseGherkin(text, uri);
586
824
  if (!gherkinDocument) {
@@ -609,10 +847,10 @@ function buildScenarioAtoms(document) {
609
847
  return document.scenarios.map((scenario) => {
610
848
  const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
611
849
  const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
612
- const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
850
+ const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
613
851
  const contractIds = /* @__PURE__ */ new Set();
614
852
  scenario.tags.forEach((tag) => {
615
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
853
+ if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
616
854
  contractIds.add(tag);
617
855
  }
618
856
  });
@@ -620,7 +858,7 @@ function buildScenarioAtoms(document) {
620
858
  for (const text of collectStepTexts(step)) {
621
859
  extractIds(text, "UI").forEach((id) => contractIds.add(id));
622
860
  extractIds(text, "API").forEach((id) => contractIds.add(id));
623
- extractIds(text, "DATA").forEach((id) => contractIds.add(id));
861
+ extractIds(text, "DB").forEach((id) => contractIds.add(id));
624
862
  }
625
863
  }
626
864
  const atom = {
@@ -699,16 +937,37 @@ function collectStepTexts(step) {
699
937
  }
700
938
  return texts;
701
939
  }
702
- function unique2(values) {
940
+ function unique3(values) {
703
941
  return Array.from(new Set(values));
704
942
  }
705
943
 
706
944
  // src/core/traceability.ts
707
945
  var SC_TAG_RE2 = /^SC-\d{4}$/;
946
+ var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
947
+ var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
948
+ "**/node_modules/**",
949
+ "**/.git/**",
950
+ "**/.qfai/**",
951
+ "**/dist/**",
952
+ "**/build/**",
953
+ "**/coverage/**",
954
+ "**/.next/**",
955
+ "**/out/**"
956
+ ];
957
+ function extractAnnotatedScIds(text) {
958
+ const ids = /* @__PURE__ */ new Set();
959
+ for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
960
+ const suffix = match[1];
961
+ if (suffix) {
962
+ ids.add(`SC-${suffix}`);
963
+ }
964
+ }
965
+ return Array.from(ids);
966
+ }
708
967
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
709
968
  const scIds = /* @__PURE__ */ new Set();
710
969
  for (const file of scenarioFiles) {
711
- const text = await readFile2(file, "utf-8");
970
+ const text = await readFile3(file, "utf-8");
712
971
  const { document, errors } = parseScenarioDocument(text, file);
713
972
  if (!document || errors.length > 0) {
714
973
  continue;
@@ -723,14 +982,67 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
723
982
  }
724
983
  return scIds;
725
984
  }
726
- async function collectScTestReferences(testsRoot) {
985
+ async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
986
+ const sources = /* @__PURE__ */ new Map();
987
+ for (const file of scenarioFiles) {
988
+ const text = await readFile3(file, "utf-8");
989
+ const { document, errors } = parseScenarioDocument(text, file);
990
+ if (!document || errors.length > 0) {
991
+ continue;
992
+ }
993
+ for (const scenario of document.scenarios) {
994
+ for (const tag of scenario.tags) {
995
+ if (!SC_TAG_RE2.test(tag)) {
996
+ continue;
997
+ }
998
+ const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
999
+ current.add(file);
1000
+ sources.set(tag, current);
1001
+ }
1002
+ }
1003
+ }
1004
+ return sources;
1005
+ }
1006
+ async function collectScTestReferences(root, globs, excludeGlobs) {
727
1007
  const refs = /* @__PURE__ */ new Map();
728
- const testFiles = await collectFiles(testsRoot, {
729
- extensions: [".ts", ".tsx", ".js", ".jsx"]
730
- });
731
- for (const file of testFiles) {
732
- const text = await readFile2(file, "utf-8");
733
- const scIds = extractIds(text, "SC");
1008
+ const normalizedGlobs = normalizeGlobs(globs);
1009
+ const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
1010
+ const mergedExcludeGlobs = Array.from(
1011
+ /* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
1012
+ );
1013
+ if (normalizedGlobs.length === 0) {
1014
+ return {
1015
+ refs,
1016
+ scan: {
1017
+ globs: normalizedGlobs,
1018
+ excludeGlobs: mergedExcludeGlobs,
1019
+ matchedFileCount: 0
1020
+ }
1021
+ };
1022
+ }
1023
+ let files = [];
1024
+ try {
1025
+ files = await collectFilesByGlobs(root, {
1026
+ globs: normalizedGlobs,
1027
+ ignore: mergedExcludeGlobs
1028
+ });
1029
+ } catch (error) {
1030
+ return {
1031
+ refs,
1032
+ scan: {
1033
+ globs: normalizedGlobs,
1034
+ excludeGlobs: mergedExcludeGlobs,
1035
+ matchedFileCount: 0
1036
+ },
1037
+ error: formatError3(error)
1038
+ };
1039
+ }
1040
+ const normalizedFiles = Array.from(
1041
+ new Set(files.map((file) => path5.normalize(file)))
1042
+ );
1043
+ for (const file of normalizedFiles) {
1044
+ const text = await readFile3(file, "utf-8");
1045
+ const scIds = extractAnnotatedScIds(text);
734
1046
  if (scIds.length === 0) {
735
1047
  continue;
736
1048
  }
@@ -740,7 +1052,14 @@ async function collectScTestReferences(testsRoot) {
740
1052
  refs.set(scId, current);
741
1053
  }
742
1054
  }
743
- return refs;
1055
+ return {
1056
+ refs,
1057
+ scan: {
1058
+ globs: normalizedGlobs,
1059
+ excludeGlobs: mergedExcludeGlobs,
1060
+ matchedFileCount: normalizedFiles.length
1061
+ }
1062
+ };
744
1063
  }
745
1064
  function buildScCoverage(scIds, refs) {
746
1065
  const sortedScIds = toSortedArray(scIds);
@@ -768,18 +1087,27 @@ function buildScCoverage(scIds, refs) {
768
1087
  function toSortedArray(values) {
769
1088
  return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
770
1089
  }
1090
+ function normalizeGlobs(globs) {
1091
+ return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1092
+ }
1093
+ function formatError3(error) {
1094
+ if (error instanceof Error) {
1095
+ return error.message;
1096
+ }
1097
+ return String(error);
1098
+ }
771
1099
 
772
1100
  // src/core/version.ts
773
- import { readFile as readFile3 } from "fs/promises";
774
- import path4 from "path";
1101
+ import { readFile as readFile4 } from "fs/promises";
1102
+ import path6 from "path";
775
1103
  import { fileURLToPath } from "url";
776
1104
  async function resolveToolVersion() {
777
- if ("0.4.0".length > 0) {
778
- return "0.4.0";
1105
+ if ("0.4.4".length > 0) {
1106
+ return "0.4.4";
779
1107
  }
780
1108
  try {
781
1109
  const packagePath = resolvePackageJsonPath();
782
- const raw = await readFile3(packagePath, "utf-8");
1110
+ const raw = await readFile4(packagePath, "utf-8");
783
1111
  const parsed = JSON.parse(raw);
784
1112
  const version = typeof parsed.version === "string" ? parsed.version : "";
785
1113
  return version.length > 0 ? version : "unknown";
@@ -790,54 +1118,23 @@ async function resolveToolVersion() {
790
1118
  function resolvePackageJsonPath() {
791
1119
  const base = import.meta.url;
792
1120
  const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
793
- return path4.resolve(path4.dirname(basePath), "../../package.json");
1121
+ return path6.resolve(path6.dirname(basePath), "../../package.json");
794
1122
  }
795
1123
 
796
1124
  // src/core/validators/contracts.ts
797
- import { readFile as readFile4 } from "fs/promises";
798
- import path6 from "path";
1125
+ import { readFile as readFile5 } from "fs/promises";
1126
+ import path8 from "path";
799
1127
 
800
1128
  // src/core/contracts.ts
801
- import path5 from "path";
1129
+ import path7 from "path";
802
1130
  import { parse as parseYaml2 } from "yaml";
803
1131
  function parseStructuredContract(file, text) {
804
- const ext = path5.extname(file).toLowerCase();
1132
+ const ext = path7.extname(file).toLowerCase();
805
1133
  if (ext === ".json") {
806
1134
  return JSON.parse(text);
807
1135
  }
808
1136
  return parseYaml2(text);
809
1137
  }
810
- function extractUiContractIds(doc) {
811
- const id = typeof doc.id === "string" ? doc.id : "";
812
- return extractIds(id, "UI");
813
- }
814
- function extractApiContractIds(doc) {
815
- const operationIds = /* @__PURE__ */ new Set();
816
- collectOperationIds(doc, operationIds);
817
- const ids = /* @__PURE__ */ new Set();
818
- for (const operationId of operationIds) {
819
- extractIds(operationId, "API").forEach((id) => ids.add(id));
820
- }
821
- return Array.from(ids);
822
- }
823
- function collectOperationIds(value, out) {
824
- if (!value || typeof value !== "object") {
825
- return;
826
- }
827
- if (Array.isArray(value)) {
828
- for (const item of value) {
829
- collectOperationIds(item, out);
830
- }
831
- return;
832
- }
833
- for (const [key, entry] of Object.entries(value)) {
834
- if (key === "operationId" && typeof entry === "string") {
835
- out.add(entry);
836
- continue;
837
- }
838
- collectOperationIds(entry, out);
839
- }
840
- }
841
1138
 
842
1139
  // src/core/validators/contracts.ts
843
1140
  var SQL_DANGEROUS_PATTERNS = [
@@ -852,9 +1149,11 @@ var SQL_DANGEROUS_PATTERNS = [
852
1149
  async function validateContracts(root, config) {
853
1150
  const issues = [];
854
1151
  const contractsRoot = resolvePath(root, config, "contractsDir");
855
- issues.push(...await validateUiContracts(path6.join(contractsRoot, "ui")));
856
- issues.push(...await validateApiContracts(path6.join(contractsRoot, "api")));
857
- issues.push(...await validateDataContracts(path6.join(contractsRoot, "db")));
1152
+ issues.push(...await validateUiContracts(path8.join(contractsRoot, "ui")));
1153
+ issues.push(...await validateApiContracts(path8.join(contractsRoot, "api")));
1154
+ issues.push(...await validateDataContracts(path8.join(contractsRoot, "db")));
1155
+ const contractIndex = await buildContractIndex(root, config);
1156
+ issues.push(...validateDuplicateContractIds(contractIndex));
858
1157
  return issues;
859
1158
  }
860
1159
  async function validateUiContracts(uiRoot) {
@@ -872,14 +1171,14 @@ async function validateUiContracts(uiRoot) {
872
1171
  }
873
1172
  const issues = [];
874
1173
  for (const file of files) {
875
- const text = await readFile4(file, "utf-8");
1174
+ const text = await readFile5(file, "utf-8");
876
1175
  const invalidIds = extractInvalidIds(text, [
877
1176
  "SPEC",
878
1177
  "BR",
879
1178
  "SC",
880
1179
  "UI",
881
1180
  "API",
882
- "DATA",
1181
+ "DB",
883
1182
  "ADR"
884
1183
  ]);
885
1184
  if (invalidIds.length > 0) {
@@ -894,32 +1193,20 @@ async function validateUiContracts(uiRoot) {
894
1193
  )
895
1194
  );
896
1195
  }
897
- let doc;
1196
+ const declaredIds = extractDeclaredContractIds(text);
1197
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
898
1198
  try {
899
- doc = parseStructuredContract(file, text);
1199
+ parseStructuredContract(file, stripContractDeclarationLines(text));
900
1200
  } catch (error) {
901
1201
  issues.push(
902
1202
  issue(
903
1203
  "QFAI-CONTRACT-001",
904
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
1204
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
905
1205
  "error",
906
1206
  file,
907
1207
  "contracts.ui.parse"
908
1208
  )
909
1209
  );
910
- continue;
911
- }
912
- const uiIds = extractUiContractIds(doc);
913
- if (uiIds.length === 0) {
914
- issues.push(
915
- issue(
916
- "QFAI-CONTRACT-002",
917
- `UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
918
- "error",
919
- file,
920
- "contracts.ui.id"
921
- )
922
- );
923
1210
  }
924
1211
  }
925
1212
  return issues;
@@ -939,14 +1226,14 @@ async function validateApiContracts(apiRoot) {
939
1226
  }
940
1227
  const issues = [];
941
1228
  for (const file of files) {
942
- const text = await readFile4(file, "utf-8");
1229
+ const text = await readFile5(file, "utf-8");
943
1230
  const invalidIds = extractInvalidIds(text, [
944
1231
  "SPEC",
945
1232
  "BR",
946
1233
  "SC",
947
1234
  "UI",
948
1235
  "API",
949
- "DATA",
1236
+ "DB",
950
1237
  "ADR"
951
1238
  ]);
952
1239
  if (invalidIds.length > 0) {
@@ -961,14 +1248,16 @@ async function validateApiContracts(apiRoot) {
961
1248
  )
962
1249
  );
963
1250
  }
1251
+ const declaredIds = extractDeclaredContractIds(text);
1252
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "API"));
964
1253
  let doc;
965
1254
  try {
966
- doc = parseStructuredContract(file, text);
1255
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
967
1256
  } catch (error) {
968
1257
  issues.push(
969
1258
  issue(
970
1259
  "QFAI-CONTRACT-001",
971
- `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
1260
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
972
1261
  "error",
973
1262
  file,
974
1263
  "contracts.api.parse"
@@ -987,18 +1276,6 @@ async function validateApiContracts(apiRoot) {
987
1276
  )
988
1277
  );
989
1278
  }
990
- const apiIds = extractApiContractIds(doc);
991
- if (apiIds.length === 0) {
992
- issues.push(
993
- issue(
994
- "QFAI-CONTRACT-002",
995
- `API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
996
- "error",
997
- file,
998
- "contracts.api.id"
999
- )
1000
- );
1001
- }
1002
1279
  }
1003
1280
  return issues;
1004
1281
  }
@@ -1007,24 +1284,24 @@ async function validateDataContracts(dataRoot) {
1007
1284
  if (files.length === 0) {
1008
1285
  return [
1009
1286
  issue(
1010
- "QFAI-DATA-000",
1011
- "DATA \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1287
+ "QFAI-DB-000",
1288
+ "DB \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1012
1289
  "info",
1013
1290
  dataRoot,
1014
- "contracts.data.files"
1291
+ "contracts.db.files"
1015
1292
  )
1016
1293
  ];
1017
1294
  }
1018
1295
  const issues = [];
1019
1296
  for (const file of files) {
1020
- const text = await readFile4(file, "utf-8");
1297
+ const text = await readFile5(file, "utf-8");
1021
1298
  const invalidIds = extractInvalidIds(text, [
1022
1299
  "SPEC",
1023
1300
  "BR",
1024
1301
  "SC",
1025
1302
  "UI",
1026
1303
  "API",
1027
- "DATA",
1304
+ "DB",
1028
1305
  "ADR"
1029
1306
  ]);
1030
1307
  if (invalidIds.length > 0) {
@@ -1039,6 +1316,8 @@ async function validateDataContracts(dataRoot) {
1039
1316
  )
1040
1317
  );
1041
1318
  }
1319
+ const declaredIds = extractDeclaredContractIds(text);
1320
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "DB"));
1042
1321
  issues.push(...lintSql(text, file));
1043
1322
  }
1044
1323
  return issues;
@@ -1049,21 +1328,87 @@ function lintSql(text, file) {
1049
1328
  if (pattern.test(text)) {
1050
1329
  issues.push(
1051
1330
  issue(
1052
- "QFAI-DATA-001",
1331
+ "QFAI-DB-001",
1053
1332
  `\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
1054
1333
  "warning",
1055
1334
  file,
1056
- "contracts.data.sql"
1335
+ "contracts.db.sql"
1057
1336
  )
1058
1337
  );
1059
1338
  }
1060
1339
  }
1061
1340
  return issues;
1062
1341
  }
1342
+ function validateDeclaredContractIds(ids, file, kind) {
1343
+ const issues = [];
1344
+ if (ids.length === 0) {
1345
+ issues.push(
1346
+ issue(
1347
+ "QFAI-CONTRACT-010",
1348
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
1349
+ "error",
1350
+ file,
1351
+ "contracts.declaration"
1352
+ )
1353
+ );
1354
+ return issues;
1355
+ }
1356
+ if (ids.length > 1) {
1357
+ issues.push(
1358
+ issue(
1359
+ "QFAI-CONTRACT-011",
1360
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B\u8907\u6570\u306E QFAI-CONTRACT-ID \u304C\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${ids.join(
1361
+ ", "
1362
+ )}`,
1363
+ "error",
1364
+ file,
1365
+ "contracts.declaration",
1366
+ ids
1367
+ )
1368
+ );
1369
+ return issues;
1370
+ }
1371
+ const [id] = ids;
1372
+ if (id && !id.startsWith(`${kind}-`)) {
1373
+ issues.push(
1374
+ issue(
1375
+ "QFAI-CONTRACT-013",
1376
+ `\u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E QFAI-CONTRACT-ID \u304C ${kind}- \u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${id}`,
1377
+ "error",
1378
+ file,
1379
+ "contracts.declarationPrefix",
1380
+ [id]
1381
+ )
1382
+ );
1383
+ }
1384
+ return issues;
1385
+ }
1386
+ function validateDuplicateContractIds(contractIndex) {
1387
+ const issues = [];
1388
+ for (const [id, files] of contractIndex.idToFiles.entries()) {
1389
+ if (files.size <= 1) {
1390
+ continue;
1391
+ }
1392
+ const sortedFiles = Array.from(files).sort((a, b) => a.localeCompare(b));
1393
+ issues.push(
1394
+ issue(
1395
+ "QFAI-CONTRACT-012",
1396
+ `\u5951\u7D04 ID \u304C\u8907\u6570\u30D5\u30A1\u30A4\u30EB\u3067\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${id} (${sortedFiles.join(
1397
+ ", "
1398
+ )})`,
1399
+ "error",
1400
+ sortedFiles[0],
1401
+ "contracts.idDuplicate",
1402
+ [id]
1403
+ )
1404
+ );
1405
+ }
1406
+ return issues;
1407
+ }
1063
1408
  function hasOpenApi(doc) {
1064
1409
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1065
1410
  }
1066
- function formatError3(error) {
1411
+ function formatError4(error) {
1067
1412
  if (error instanceof Error) {
1068
1413
  return error.message;
1069
1414
  }
@@ -1088,279 +1433,98 @@ function issue(code, message, severity, file, rule, refs) {
1088
1433
  }
1089
1434
 
1090
1435
  // src/core/validators/delta.ts
1091
- import { readFile as readFile5 } from "fs/promises";
1092
- import path7 from "path";
1436
+ import { readFile as readFile6 } from "fs/promises";
1437
+ import path9 from "path";
1093
1438
  var SECTION_RE = /^##\s+変更区分/m;
1094
1439
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1095
1440
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
1096
1441
  var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
1097
1442
  var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
1098
1443
  async function validateDeltas(root, config) {
1099
- const specsRoot = resolvePath(root, config, "specsDir");
1100
- const packs = await collectSpecPackDirs(specsRoot);
1101
- if (packs.length === 0) {
1102
- return [];
1103
- }
1104
- const issues = [];
1105
- for (const pack of packs) {
1106
- const deltaPath = path7.join(pack, "delta.md");
1107
- let text;
1108
- try {
1109
- text = await readFile5(deltaPath, "utf-8");
1110
- } catch (error) {
1111
- if (isMissingFileError2(error)) {
1112
- issues.push(
1113
- issue2(
1114
- "QFAI-DELTA-001",
1115
- "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1116
- "error",
1117
- deltaPath,
1118
- "delta.exists"
1119
- )
1120
- );
1121
- continue;
1122
- }
1123
- throw error;
1124
- }
1125
- const hasSection = SECTION_RE.test(text);
1126
- const hasCompatibility = COMPAT_LINE_RE.test(text);
1127
- const hasChange = CHANGE_LINE_RE.test(text);
1128
- if (!hasSection || !hasCompatibility || !hasChange) {
1129
- issues.push(
1130
- issue2(
1131
- "QFAI-DELTA-002",
1132
- "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
1133
- "error",
1134
- deltaPath,
1135
- "delta.section"
1136
- )
1137
- );
1138
- continue;
1139
- }
1140
- const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
1141
- const changeChecked = CHANGE_CHECKED_RE.test(text);
1142
- if (compatibilityChecked === changeChecked) {
1143
- issues.push(
1144
- issue2(
1145
- "QFAI-DELTA-003",
1146
- "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",
1147
- "error",
1148
- deltaPath,
1149
- "delta.classification"
1150
- )
1151
- );
1152
- }
1153
- }
1154
- return issues;
1155
- }
1156
- function isMissingFileError2(error) {
1157
- if (!error || typeof error !== "object") {
1158
- return false;
1159
- }
1160
- return error.code === "ENOENT";
1161
- }
1162
- function issue2(code, message, severity, file, rule, refs) {
1163
- const issue7 = {
1164
- code,
1165
- severity,
1166
- message
1167
- };
1168
- if (file) {
1169
- issue7.file = file;
1170
- }
1171
- if (rule) {
1172
- issue7.rule = rule;
1173
- }
1174
- if (refs && refs.length > 0) {
1175
- issue7.refs = refs;
1176
- }
1177
- return issue7;
1178
- }
1179
-
1180
- // src/core/validators/ids.ts
1181
- import { readFile as readFile7 } from "fs/promises";
1182
- import path9 from "path";
1183
-
1184
- // src/core/contractIndex.ts
1185
- import { readFile as readFile6 } from "fs/promises";
1186
- import path8 from "path";
1187
- async function buildContractIndex(root, config) {
1188
- const contractsRoot = resolvePath(root, config, "contractsDir");
1189
- const uiRoot = path8.join(contractsRoot, "ui");
1190
- const apiRoot = path8.join(contractsRoot, "api");
1191
- const dataRoot = path8.join(contractsRoot, "db");
1192
- const [uiFiles, apiFiles, dataFiles] = await Promise.all([
1193
- collectUiContractFiles(uiRoot),
1194
- collectApiContractFiles(apiRoot),
1195
- collectDataContractFiles(dataRoot)
1196
- ]);
1197
- const index = {
1198
- ids: /* @__PURE__ */ new Set(),
1199
- idToFiles: /* @__PURE__ */ new Map(),
1200
- files: { ui: uiFiles, api: apiFiles, data: dataFiles },
1201
- structuredParseFailedFiles: /* @__PURE__ */ new Set()
1202
- };
1203
- await indexUiContracts(uiFiles, index);
1204
- await indexApiContracts(apiFiles, index);
1205
- await indexDataContracts(dataFiles, index);
1206
- return index;
1207
- }
1208
- async function indexUiContracts(files, index) {
1209
- for (const file of files) {
1210
- const text = await readFile6(file, "utf-8");
1211
- try {
1212
- const doc = parseStructuredContract(file, text);
1213
- extractUiContractIds(doc).forEach((id) => record(index, id, file));
1214
- } catch {
1215
- index.structuredParseFailedFiles.add(file);
1216
- extractIds(text, "UI").forEach((id) => record(index, id, file));
1217
- }
1218
- }
1219
- }
1220
- async function indexApiContracts(files, index) {
1221
- for (const file of files) {
1222
- const text = await readFile6(file, "utf-8");
1223
- try {
1224
- const doc = parseStructuredContract(file, text);
1225
- extractApiContractIds(doc).forEach((id) => record(index, id, file));
1226
- } catch {
1227
- index.structuredParseFailedFiles.add(file);
1228
- extractIds(text, "API").forEach((id) => record(index, id, file));
1229
- }
1230
- }
1231
- }
1232
- async function indexDataContracts(files, index) {
1233
- for (const file of files) {
1234
- const text = await readFile6(file, "utf-8");
1235
- extractIds(text, "DATA").forEach((id) => record(index, id, file));
1236
- }
1237
- }
1238
- function record(index, id, file) {
1239
- index.ids.add(id);
1240
- const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
1241
- current.add(file);
1242
- index.idToFiles.set(id, current);
1243
- }
1244
-
1245
- // src/core/parse/markdown.ts
1246
- var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1247
- function parseHeadings(md) {
1248
- const lines = md.split(/\r?\n/);
1249
- const headings = [];
1250
- for (let i = 0; i < lines.length; i++) {
1251
- const line = lines[i] ?? "";
1252
- const match = line.match(HEADING_RE);
1253
- if (!match) continue;
1254
- const levelToken = match[1];
1255
- const title = match[2];
1256
- if (!levelToken || !title) continue;
1257
- headings.push({
1258
- level: levelToken.length,
1259
- title: title.trim(),
1260
- line: i + 1
1261
- });
1262
- }
1263
- return headings;
1264
- }
1265
- function extractH2Sections(md) {
1266
- const lines = md.split(/\r?\n/);
1267
- const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1268
- const sections = /* @__PURE__ */ new Map();
1269
- for (let i = 0; i < headings.length; i++) {
1270
- const current = headings[i];
1271
- if (!current) continue;
1272
- const next = headings[i + 1];
1273
- const startLine = current.line + 1;
1274
- const endLine = (next?.line ?? lines.length + 1) - 1;
1275
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1276
- sections.set(current.title.trim(), {
1277
- title: current.title.trim(),
1278
- startLine,
1279
- endLine,
1280
- body
1281
- });
1282
- }
1283
- return sections;
1284
- }
1285
-
1286
- // src/core/parse/spec.ts
1287
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1288
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1289
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1290
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1291
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1292
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1293
- function parseSpec(md, file) {
1294
- const headings = parseHeadings(md);
1295
- const h1 = headings.find((heading) => heading.level === 1);
1296
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1297
- const sections = extractH2Sections(md);
1298
- const sectionNames = new Set(Array.from(sections.keys()));
1299
- const brSection = sections.get(BR_SECTION_TITLE);
1300
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1301
- const startLine = brSection?.startLine ?? 1;
1302
- const brs = [];
1303
- const brsWithoutPriority = [];
1304
- const brsWithInvalidPriority = [];
1305
- for (let i = 0; i < brLines.length; i++) {
1306
- const lineText = brLines[i] ?? "";
1307
- const lineNumber = startLine + i;
1308
- const validMatch = lineText.match(BR_LINE_RE);
1309
- if (validMatch) {
1310
- const id = validMatch[1];
1311
- const priority = validMatch[2];
1312
- const text = validMatch[3];
1313
- if (!id || !priority || !text) continue;
1314
- brs.push({
1315
- id,
1316
- priority,
1317
- text: text.trim(),
1318
- line: lineNumber
1319
- });
1320
- continue;
1321
- }
1322
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1323
- if (anyPriorityMatch) {
1324
- const id = anyPriorityMatch[1];
1325
- const priority = anyPriorityMatch[2];
1326
- const text = anyPriorityMatch[3];
1327
- if (!id || !priority || !text) continue;
1328
- if (!VALID_PRIORITIES.has(priority)) {
1329
- brsWithInvalidPriority.push({
1330
- id,
1331
- priority,
1332
- text: text.trim(),
1333
- line: lineNumber
1334
- });
1444
+ const specsRoot = resolvePath(root, config, "specsDir");
1445
+ const packs = await collectSpecPackDirs(specsRoot);
1446
+ if (packs.length === 0) {
1447
+ return [];
1448
+ }
1449
+ const issues = [];
1450
+ for (const pack of packs) {
1451
+ const deltaPath = path9.join(pack, "delta.md");
1452
+ let text;
1453
+ try {
1454
+ text = await readFile6(deltaPath, "utf-8");
1455
+ } catch (error) {
1456
+ if (isMissingFileError2(error)) {
1457
+ issues.push(
1458
+ issue2(
1459
+ "QFAI-DELTA-001",
1460
+ "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1461
+ "error",
1462
+ deltaPath,
1463
+ "delta.exists"
1464
+ )
1465
+ );
1466
+ continue;
1335
1467
  }
1468
+ throw error;
1469
+ }
1470
+ const hasSection = SECTION_RE.test(text);
1471
+ const hasCompatibility = COMPAT_LINE_RE.test(text);
1472
+ const hasChange = CHANGE_LINE_RE.test(text);
1473
+ if (!hasSection || !hasCompatibility || !hasChange) {
1474
+ issues.push(
1475
+ issue2(
1476
+ "QFAI-DELTA-002",
1477
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
1478
+ "error",
1479
+ deltaPath,
1480
+ "delta.section"
1481
+ )
1482
+ );
1336
1483
  continue;
1337
1484
  }
1338
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1339
- if (noPriorityMatch) {
1340
- const id = noPriorityMatch[1];
1341
- const text = noPriorityMatch[2];
1342
- if (!id || !text) continue;
1343
- brsWithoutPriority.push({
1344
- id,
1345
- text: text.trim(),
1346
- line: lineNumber
1347
- });
1485
+ const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
1486
+ const changeChecked = CHANGE_CHECKED_RE.test(text);
1487
+ if (compatibilityChecked === changeChecked) {
1488
+ issues.push(
1489
+ issue2(
1490
+ "QFAI-DELTA-003",
1491
+ "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",
1492
+ "error",
1493
+ deltaPath,
1494
+ "delta.classification"
1495
+ )
1496
+ );
1348
1497
  }
1349
1498
  }
1350
- const parsed = {
1351
- file,
1352
- sections: sectionNames,
1353
- brs,
1354
- brsWithoutPriority,
1355
- brsWithInvalidPriority
1499
+ return issues;
1500
+ }
1501
+ function isMissingFileError2(error) {
1502
+ if (!error || typeof error !== "object") {
1503
+ return false;
1504
+ }
1505
+ return error.code === "ENOENT";
1506
+ }
1507
+ function issue2(code, message, severity, file, rule, refs) {
1508
+ const issue7 = {
1509
+ code,
1510
+ severity,
1511
+ message
1356
1512
  };
1357
- if (specId) {
1358
- parsed.specId = specId;
1513
+ if (file) {
1514
+ issue7.file = file;
1359
1515
  }
1360
- return parsed;
1516
+ if (rule) {
1517
+ issue7.rule = rule;
1518
+ }
1519
+ if (refs && refs.length > 0) {
1520
+ issue7.refs = refs;
1521
+ }
1522
+ return issue7;
1361
1523
  }
1362
1524
 
1363
1525
  // src/core/validators/ids.ts
1526
+ import { readFile as readFile7 } from "fs/promises";
1527
+ import path10 from "path";
1364
1528
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1365
1529
  async function validateDefinedIds(root, config) {
1366
1530
  const issues = [];
@@ -1426,7 +1590,7 @@ function recordId(out, id, file) {
1426
1590
  }
1427
1591
  function formatFileList(files, root) {
1428
1592
  return files.map((file) => {
1429
- const relative = path9.relative(root, file);
1593
+ const relative = path10.relative(root, file);
1430
1594
  return relative.length > 0 ? relative : file;
1431
1595
  }).join(", ");
1432
1596
  }
@@ -1455,7 +1619,6 @@ var WHEN_PATTERN = /\bWhen\b/;
1455
1619
  var THEN_PATTERN = /\bThen\b/;
1456
1620
  var SC_TAG_RE4 = /^SC-\d{4}$/;
1457
1621
  var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
1458
- var BR_TAG_RE2 = /^BR-\d{4}$/;
1459
1622
  async function validateScenarios(root, config) {
1460
1623
  const specsRoot = resolvePath(root, config, "specsDir");
1461
1624
  const entries = await collectSpecEntries(specsRoot);
@@ -1504,7 +1667,7 @@ function validateScenarioContent(text, file) {
1504
1667
  "SC",
1505
1668
  "UI",
1506
1669
  "API",
1507
- "DATA",
1670
+ "DB",
1508
1671
  "ADR"
1509
1672
  ]);
1510
1673
  if (invalidIds.length > 0) {
@@ -1535,17 +1698,7 @@ function validateScenarioContent(text, file) {
1535
1698
  const featureSpecTags = document.featureTags.filter(
1536
1699
  (tag) => SPEC_TAG_RE2.test(tag)
1537
1700
  );
1538
- if (featureSpecTags.length === 0) {
1539
- issues.push(
1540
- issue4(
1541
- "QFAI-SC-009",
1542
- "Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1543
- "error",
1544
- file,
1545
- "scenario.featureSpec"
1546
- )
1547
- );
1548
- } else if (featureSpecTags.length > 1) {
1701
+ if (featureSpecTags.length > 1) {
1549
1702
  issues.push(
1550
1703
  issue4(
1551
1704
  "QFAI-SC-009",
@@ -1573,17 +1726,6 @@ function validateScenarioContent(text, file) {
1573
1726
  )
1574
1727
  );
1575
1728
  }
1576
- if (document.scenarios.length > 1) {
1577
- issues.push(
1578
- issue4(
1579
- "QFAI-SC-011",
1580
- `Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
1581
- "error",
1582
- file,
1583
- "scenario.single"
1584
- )
1585
- );
1586
- }
1587
1729
  for (const scenario of document.scenarios) {
1588
1730
  if (scenario.tags.length === 0) {
1589
1731
  issues.push(
@@ -1604,12 +1746,6 @@ function validateScenarioContent(text, file) {
1604
1746
  } else if (scTags.length > 1) {
1605
1747
  missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
1606
1748
  }
1607
- if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
1608
- missingTags.push("SPEC");
1609
- }
1610
- if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
1611
- missingTags.push("BR");
1612
- }
1613
1749
  if (missingTags.length > 0) {
1614
1750
  issues.push(
1615
1751
  issue4(
@@ -1729,7 +1865,7 @@ function validateSpecContent(text, file, requiredSections) {
1729
1865
  "SC",
1730
1866
  "UI",
1731
1867
  "API",
1732
- "DATA",
1868
+ "DB",
1733
1869
  "ADR"
1734
1870
  ]);
1735
1871
  if (invalidIds.length > 0) {
@@ -1844,9 +1980,8 @@ function isMissingFileError4(error) {
1844
1980
 
1845
1981
  // src/core/validators/traceability.ts
1846
1982
  import { readFile as readFile10 } from "fs/promises";
1847
- var SC_TAG_RE5 = /^SC-\d{4}$/;
1848
1983
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
1849
- var BR_TAG_RE3 = /^BR-\d{4}$/;
1984
+ var BR_TAG_RE2 = /^BR-\d{4}$/;
1850
1985
  async function validateTraceability(root, config) {
1851
1986
  const issues = [];
1852
1987
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1859,8 +1994,7 @@ async function validateTraceability(root, config) {
1859
1994
  const brIdsInSpecs = /* @__PURE__ */ new Set();
1860
1995
  const brIdsInScenarios = /* @__PURE__ */ new Set();
1861
1996
  const scIdsInScenarios = /* @__PURE__ */ new Set();
1862
- const scenarioContractIds = /* @__PURE__ */ new Set();
1863
- const scWithContracts = /* @__PURE__ */ new Set();
1997
+ const specContractIds = /* @__PURE__ */ new Set();
1864
1998
  const specToBrIds = /* @__PURE__ */ new Map();
1865
1999
  const contractIndex = await buildContractIndex(root, config);
1866
2000
  const contractIds = contractIndex.ids;
@@ -1873,19 +2007,60 @@ async function validateTraceability(root, config) {
1873
2007
  }
1874
2008
  const brIds = parsed.brs.map((br) => br.id);
1875
2009
  brIds.forEach((id) => brIdsInSpecs.add(id));
1876
- const referencedContractIds = /* @__PURE__ */ new Set([
1877
- ...extractIds(text, "UI"),
1878
- ...extractIds(text, "API"),
1879
- ...extractIds(text, "DATA")
1880
- ]);
1881
- const unknownContractIds = Array.from(referencedContractIds).filter(
2010
+ if (parsed.specId) {
2011
+ const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
2012
+ brIds.forEach((id) => current.add(id));
2013
+ specToBrIds.set(parsed.specId, current);
2014
+ }
2015
+ const contractRefs = parsed.contractRefs;
2016
+ if (contractRefs.lines.length === 0) {
2017
+ issues.push(
2018
+ issue6(
2019
+ "QFAI-TRACE-020",
2020
+ "Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2021
+ "error",
2022
+ file,
2023
+ "traceability.specContractRefRequired"
2024
+ )
2025
+ );
2026
+ } else {
2027
+ if (contractRefs.hasNone && contractRefs.ids.length > 0) {
2028
+ issues.push(
2029
+ issue6(
2030
+ "QFAI-TRACE-021",
2031
+ "Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2032
+ "error",
2033
+ file,
2034
+ "traceability.specContractRefFormat"
2035
+ )
2036
+ );
2037
+ }
2038
+ if (contractRefs.invalidTokens.length > 0) {
2039
+ issues.push(
2040
+ issue6(
2041
+ "QFAI-TRACE-021",
2042
+ `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
2043
+ ", "
2044
+ )}`,
2045
+ "error",
2046
+ file,
2047
+ "traceability.specContractRefFormat",
2048
+ contractRefs.invalidTokens
2049
+ )
2050
+ );
2051
+ }
2052
+ }
2053
+ contractRefs.ids.forEach((id) => {
2054
+ specContractIds.add(id);
2055
+ });
2056
+ const unknownContractIds = contractRefs.ids.filter(
1882
2057
  (id) => !contractIds.has(id)
1883
2058
  );
1884
2059
  if (unknownContractIds.length > 0) {
1885
2060
  issues.push(
1886
2061
  issue6(
1887
- "QFAI-TRACE-009",
1888
- `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2062
+ "QFAI-TRACE-021",
2063
+ `Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1889
2064
  ", "
1890
2065
  )}`,
1891
2066
  "error",
@@ -1895,11 +2070,6 @@ async function validateTraceability(root, config) {
1895
2070
  )
1896
2071
  );
1897
2072
  }
1898
- if (parsed.specId) {
1899
- const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
1900
- brIds.forEach((id) => current.add(id));
1901
- specToBrIds.set(parsed.specId, current);
1902
- }
1903
2073
  }
1904
2074
  for (const file of scenarioFiles) {
1905
2075
  const text = await readFile10(file, "utf-8");
@@ -1909,20 +2079,42 @@ async function validateTraceability(root, config) {
1909
2079
  continue;
1910
2080
  }
1911
2081
  const atoms = buildScenarioAtoms(document);
2082
+ const scIdsInFile = /* @__PURE__ */ new Set();
1912
2083
  for (const [index, scenario] of document.scenarios.entries()) {
1913
2084
  const atom = atoms[index];
1914
2085
  if (!atom) {
1915
2086
  continue;
1916
2087
  }
1917
2088
  const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
1918
- const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
1919
- const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.test(tag));
1920
- brTags.forEach((id) => brIdsInScenarios.add(id));
1921
- scTags.forEach((id) => scIdsInScenarios.add(id));
1922
- atom.contractIds.forEach((id) => scenarioContractIds.add(id));
1923
- if (atom.contractIds.length > 0) {
1924
- scTags.forEach((id) => scWithContracts.add(id));
2089
+ const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
2090
+ const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
2091
+ if (specTags.length === 0) {
2092
+ issues.push(
2093
+ issue6(
2094
+ "QFAI-TRACE-014",
2095
+ `Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2096
+ "error",
2097
+ file,
2098
+ "traceability.scenarioSpecRequired"
2099
+ )
2100
+ );
2101
+ }
2102
+ if (brTags.length === 0) {
2103
+ issues.push(
2104
+ issue6(
2105
+ "QFAI-TRACE-015",
2106
+ `Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
2107
+ "error",
2108
+ file,
2109
+ "traceability.scenarioBrRequired"
2110
+ )
2111
+ );
1925
2112
  }
2113
+ brTags.forEach((id) => brIdsInScenarios.add(id));
2114
+ scTags.forEach((id) => {
2115
+ scIdsInScenarios.add(id);
2116
+ scIdsInFile.add(id);
2117
+ });
1926
2118
  const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
1927
2119
  if (unknownSpecIds.length > 0) {
1928
2120
  issues.push(
@@ -1996,6 +2188,22 @@ async function validateTraceability(root, config) {
1996
2188
  }
1997
2189
  }
1998
2190
  }
2191
+ if (scIdsInFile.size !== 1) {
2192
+ const invalidScIds = Array.from(scIdsInFile).sort(
2193
+ (a, b) => a.localeCompare(b)
2194
+ );
2195
+ 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(", ")}`;
2196
+ issues.push(
2197
+ issue6(
2198
+ "QFAI-TRACE-012",
2199
+ `Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
2200
+ "error",
2201
+ file,
2202
+ "traceability.specScOneToOne",
2203
+ invalidScIds
2204
+ )
2205
+ );
2206
+ }
1999
2207
  }
2000
2208
  if (upstreamIds.size === 0) {
2001
2209
  return [
@@ -2025,40 +2233,62 @@ async function validateTraceability(root, config) {
2025
2233
  );
2026
2234
  }
2027
2235
  }
2028
- if (config.validation.traceability.scMustTouchContracts && scIdsInScenarios.size > 0) {
2029
- const scWithoutContracts = Array.from(scIdsInScenarios).filter(
2030
- (id) => !scWithContracts.has(id)
2236
+ const scRefsResult = await collectScTestReferences(
2237
+ root,
2238
+ config.validation.traceability.testFileGlobs,
2239
+ config.validation.traceability.testFileExcludeGlobs
2240
+ );
2241
+ const scTestRefs = scRefsResult.refs;
2242
+ const testFileScan = scRefsResult.scan;
2243
+ const hasScenarios = scIdsInScenarios.size > 0;
2244
+ const hasGlobConfig = testFileScan.globs.length > 0;
2245
+ const hasMatchedTests = testFileScan.matchedFileCount > 0;
2246
+ if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
2247
+ const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
2248
+ issues.push(
2249
+ issue6(
2250
+ "QFAI-TRACE-013",
2251
+ `\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}`,
2252
+ "error",
2253
+ testsRoot,
2254
+ "traceability.testFileGlobs"
2255
+ )
2256
+ );
2257
+ } else {
2258
+ if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2259
+ const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2260
+ const refs = scTestRefs.get(id);
2261
+ return !refs || refs.size === 0;
2262
+ });
2263
+ if (scWithoutTests.length > 0) {
2264
+ issues.push(
2265
+ issue6(
2266
+ "QFAI-TRACE-010",
2267
+ `SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
2268
+ ", "
2269
+ )}\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`,
2270
+ config.validation.traceability.scNoTestSeverity,
2271
+ testsRoot,
2272
+ "traceability.scMustHaveTest",
2273
+ scWithoutTests
2274
+ )
2275
+ );
2276
+ }
2277
+ }
2278
+ const unknownScIds = Array.from(scTestRefs.keys()).filter(
2279
+ (id) => !scIdsInScenarios.has(id)
2031
2280
  );
2032
- if (scWithoutContracts.length > 0) {
2281
+ if (unknownScIds.length > 0) {
2033
2282
  issues.push(
2034
2283
  issue6(
2035
- "QFAI_TRACE_SC_NO_CONTRACT",
2036
- `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
2284
+ "QFAI-TRACE-011",
2285
+ `\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(
2037
2286
  ", "
2038
2287
  )}`,
2039
2288
  "error",
2040
- specsRoot,
2041
- "traceability.scMustTouchContracts",
2042
- scWithoutContracts
2043
- )
2044
- );
2045
- }
2046
- }
2047
- if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
2048
- const scTestRefs = await collectScTestReferences(testsRoot);
2049
- const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
2050
- const refs = scTestRefs.get(id);
2051
- return !refs || refs.size === 0;
2052
- });
2053
- if (scWithoutTests.length > 0) {
2054
- issues.push(
2055
- issue6(
2056
- "QFAI-TRACE-010",
2057
- `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`,
2058
- config.validation.traceability.scNoTestSeverity,
2059
2289
  testsRoot,
2060
- "traceability.scMustHaveTest",
2061
- scWithoutTests
2290
+ "traceability.scUnknownInTests",
2291
+ unknownScIds
2062
2292
  )
2063
2293
  );
2064
2294
  }
@@ -2066,16 +2296,16 @@ async function validateTraceability(root, config) {
2066
2296
  if (!config.validation.traceability.allowOrphanContracts) {
2067
2297
  if (contractIds.size > 0) {
2068
2298
  const orphanContracts = Array.from(contractIds).filter(
2069
- (id) => !scenarioContractIds.has(id)
2299
+ (id) => !specContractIds.has(id)
2070
2300
  );
2071
2301
  if (orphanContracts.length > 0) {
2072
2302
  issues.push(
2073
2303
  issue6(
2074
- "QFAI_CONTRACT_ORPHAN",
2075
- `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2304
+ "QFAI-TRACE-022",
2305
+ `\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2076
2306
  "error",
2077
2307
  specsRoot,
2078
- "traceability.allowOrphanContracts",
2308
+ "traceability.contractCoverage",
2079
2309
  orphanContracts
2080
2310
  )
2081
2311
  );
@@ -2121,8 +2351,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2121
2351
  issues.push(
2122
2352
  issue6(
2123
2353
  "QFAI-TRACE-002",
2124
- "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
2125
- "warning",
2354
+ "\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",
2355
+ "info",
2126
2356
  srcRoot,
2127
2357
  "traceability.codeReferences"
2128
2358
  )
@@ -2165,11 +2395,24 @@ async function validateProject(root, configResult) {
2165
2395
  ...await validateDefinedIds(root, config),
2166
2396
  ...await validateTraceability(root, config)
2167
2397
  ];
2398
+ const specsRoot = resolvePath(root, config, "specsDir");
2399
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
2400
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2401
+ const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
2402
+ root,
2403
+ config.validation.traceability.testFileGlobs,
2404
+ config.validation.traceability.testFileExcludeGlobs
2405
+ );
2406
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
2168
2407
  const toolVersion = await resolveToolVersion();
2169
2408
  return {
2170
2409
  toolVersion,
2171
2410
  issues,
2172
- counts: countIssues(issues)
2411
+ counts: countIssues(issues),
2412
+ traceability: {
2413
+ sc: scCoverage,
2414
+ testFiles
2415
+ }
2173
2416
  };
2174
2417
  }
2175
2418
  function countIssues(issues) {
@@ -2183,16 +2426,16 @@ function countIssues(issues) {
2183
2426
  }
2184
2427
 
2185
2428
  // src/core/report.ts
2186
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
2429
+ var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2187
2430
  async function createReportData(root, validation, configResult) {
2188
2431
  const resolved = configResult ?? await loadConfig(root);
2189
2432
  const config = resolved.config;
2190
2433
  const configPath = resolved.configPath;
2191
2434
  const specsRoot = resolvePath(root, config, "specsDir");
2192
2435
  const contractsRoot = resolvePath(root, config, "contractsDir");
2193
- const apiRoot = path10.join(contractsRoot, "api");
2194
- const uiRoot = path10.join(contractsRoot, "ui");
2195
- const dbRoot = path10.join(contractsRoot, "db");
2436
+ const apiRoot = path11.join(contractsRoot, "api");
2437
+ const uiRoot = path11.join(contractsRoot, "ui");
2438
+ const dbRoot = path11.join(contractsRoot, "db");
2196
2439
  const srcRoot = resolvePath(root, config, "srcDir");
2197
2440
  const testsRoot = resolvePath(root, config, "testsDir");
2198
2441
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2202,6 +2445,23 @@ async function createReportData(root, validation, configResult) {
2202
2445
  ui: uiFiles,
2203
2446
  db: dbFiles
2204
2447
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
2448
+ const contractIndex = await buildContractIndex(root, config);
2449
+ const specContractRefs = await collectSpecContractRefs(specFiles);
2450
+ const contractIdList = Array.from(contractIndex.ids);
2451
+ const referencedContracts = /* @__PURE__ */ new Set();
2452
+ for (const ids of specContractRefs.specToContractIds.values()) {
2453
+ ids.forEach((id) => referencedContracts.add(id));
2454
+ }
2455
+ const referencedContractCount = contractIdList.filter(
2456
+ (id) => referencedContracts.has(id)
2457
+ ).length;
2458
+ const orphanContractCount = contractIdList.filter(
2459
+ (id) => !referencedContracts.has(id)
2460
+ ).length;
2461
+ const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
2462
+ const specToContractIdsRecord = mapToSortedRecord(
2463
+ specContractRefs.specToContractIds
2464
+ );
2205
2465
  const idsByPrefix = await collectIds([
2206
2466
  ...specFiles,
2207
2467
  ...scenarioFiles,
@@ -2219,8 +2479,15 @@ async function createReportData(root, validation, configResult) {
2219
2479
  testsRoot
2220
2480
  );
2221
2481
  const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2222
- const scTestRefs = await collectScTestReferences(testsRoot);
2223
- const scCoverage = buildScCoverage(scIds, scTestRefs);
2482
+ const scRefsResult = await collectScTestReferences(
2483
+ root,
2484
+ config.validation.traceability.testFileGlobs,
2485
+ config.validation.traceability.testFileExcludeGlobs
2486
+ );
2487
+ const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2488
+ const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2489
+ const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2490
+ const scSourceRecord = mapToSortedRecord(scSources);
2224
2491
  const resolvedValidation = validation ?? await validateProject(root, resolved);
2225
2492
  const version = await resolveToolVersion();
2226
2493
  return {
@@ -2245,12 +2512,24 @@ async function createReportData(root, validation, configResult) {
2245
2512
  sc: idsByPrefix.SC,
2246
2513
  ui: idsByPrefix.UI,
2247
2514
  api: idsByPrefix.API,
2248
- data: idsByPrefix.DATA
2515
+ db: idsByPrefix.DB
2249
2516
  },
2250
2517
  traceability: {
2251
2518
  upstreamIdsFound: upstreamIds.size,
2252
2519
  referencedInCodeOrTests: traceability,
2253
- sc: scCoverage
2520
+ sc: scCoverage,
2521
+ scSources: scSourceRecord,
2522
+ testFiles,
2523
+ contracts: {
2524
+ total: contractIdList.length,
2525
+ referenced: referencedContractCount,
2526
+ orphan: orphanContractCount,
2527
+ idToSpecs: contractIdToSpecsRecord
2528
+ },
2529
+ specs: {
2530
+ contractRefMissing: specContractRefs.missingRefSpecs.size,
2531
+ specToContractIds: specToContractIdsRecord
2532
+ }
2254
2533
  },
2255
2534
  issues: resolvedValidation.issues
2256
2535
  };
@@ -2279,7 +2558,7 @@ function formatReportMarkdown(data) {
2279
2558
  lines.push(formatIdLine("SC", data.ids.sc));
2280
2559
  lines.push(formatIdLine("UI", data.ids.ui));
2281
2560
  lines.push(formatIdLine("API", data.ids.api));
2282
- lines.push(formatIdLine("DATA", data.ids.data));
2561
+ lines.push(formatIdLine("DB", data.ids.db));
2283
2562
  lines.push("");
2284
2563
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
2285
2564
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
@@ -2287,14 +2566,77 @@ function formatReportMarkdown(data) {
2287
2566
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2288
2567
  );
2289
2568
  lines.push("");
2569
+ lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
2570
+ lines.push(`- total: ${data.traceability.contracts.total}`);
2571
+ lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
2572
+ lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
2573
+ lines.push(
2574
+ `- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
2575
+ );
2576
+ lines.push("");
2577
+ lines.push("## \u5951\u7D04\u2192Spec");
2578
+ const contractToSpecs = data.traceability.contracts.idToSpecs;
2579
+ const contractIds = Object.keys(contractToSpecs).sort(
2580
+ (a, b) => a.localeCompare(b)
2581
+ );
2582
+ if (contractIds.length === 0) {
2583
+ lines.push("- (none)");
2584
+ } else {
2585
+ for (const contractId of contractIds) {
2586
+ const specs = contractToSpecs[contractId] ?? [];
2587
+ if (specs.length === 0) {
2588
+ lines.push(`- ${contractId}: (none)`);
2589
+ } else {
2590
+ lines.push(`- ${contractId}: ${specs.join(", ")}`);
2591
+ }
2592
+ }
2593
+ }
2594
+ lines.push("");
2595
+ lines.push("## Spec\u2192\u5951\u7D04");
2596
+ const specToContracts = data.traceability.specs.specToContractIds;
2597
+ const specIds = Object.keys(specToContracts).sort(
2598
+ (a, b) => a.localeCompare(b)
2599
+ );
2600
+ if (specIds.length === 0) {
2601
+ lines.push("- (none)");
2602
+ } else {
2603
+ for (const specId of specIds) {
2604
+ const contractIds2 = specToContracts[specId] ?? [];
2605
+ if (contractIds2.length === 0) {
2606
+ lines.push(`- ${specId}: (none)`);
2607
+ } else {
2608
+ lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
2609
+ }
2610
+ }
2611
+ }
2612
+ lines.push("");
2290
2613
  lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2291
2614
  lines.push(`- total: ${data.traceability.sc.total}`);
2292
2615
  lines.push(`- covered: ${data.traceability.sc.covered}`);
2293
2616
  lines.push(`- missing: ${data.traceability.sc.missing}`);
2617
+ lines.push(
2618
+ `- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
2619
+ );
2620
+ lines.push(
2621
+ `- testFileExcludeGlobs: ${formatList(
2622
+ data.traceability.testFiles.excludeGlobs
2623
+ )}`
2624
+ );
2625
+ lines.push(
2626
+ `- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
2627
+ );
2294
2628
  if (data.traceability.sc.missingIds.length === 0) {
2295
2629
  lines.push("- missingIds: (none)");
2296
2630
  } else {
2297
- lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
2631
+ const sources = data.traceability.scSources;
2632
+ const missingWithSources = data.traceability.sc.missingIds.map((id) => {
2633
+ const files = sources[id] ?? [];
2634
+ if (files.length === 0) {
2635
+ return id;
2636
+ }
2637
+ return `${id} (${files.join(", ")})`;
2638
+ });
2639
+ lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
2298
2640
  }
2299
2641
  lines.push("");
2300
2642
  lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
@@ -2313,6 +2655,20 @@ function formatReportMarkdown(data) {
2313
2655
  }
2314
2656
  }
2315
2657
  lines.push("");
2658
+ lines.push("## Spec:SC=1:1 \u9055\u53CD");
2659
+ const specScIssues = data.issues.filter(
2660
+ (item) => item.code === "QFAI-TRACE-012"
2661
+ );
2662
+ if (specScIssues.length === 0) {
2663
+ lines.push("- (none)");
2664
+ } else {
2665
+ for (const item of specScIssues) {
2666
+ const location = item.file ?? "(unknown)";
2667
+ const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
2668
+ lines.push(`- ${location}: ${refs}`);
2669
+ }
2670
+ }
2671
+ lines.push("");
2316
2672
  lines.push("## Hotspots");
2317
2673
  const hotspots = buildHotspots(data.issues);
2318
2674
  if (hotspots.length === 0) {
@@ -2327,7 +2683,7 @@ function formatReportMarkdown(data) {
2327
2683
  lines.push("");
2328
2684
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
2329
2685
  const traceIssues = data.issues.filter(
2330
- (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-") || item.code === "QFAI_CONTRACT_ORPHAN"
2686
+ (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
2331
2687
  );
2332
2688
  if (traceIssues.length === 0) {
2333
2689
  lines.push("- (none)");
@@ -2357,6 +2713,33 @@ function formatReportMarkdown(data) {
2357
2713
  function formatReportJson(data) {
2358
2714
  return JSON.stringify(data, null, 2);
2359
2715
  }
2716
+ async function collectSpecContractRefs(specFiles) {
2717
+ const specToContractIds = /* @__PURE__ */ new Map();
2718
+ const idToSpecs = /* @__PURE__ */ new Map();
2719
+ const missingRefSpecs = /* @__PURE__ */ new Set();
2720
+ for (const file of specFiles) {
2721
+ const text = await readFile11(file, "utf-8");
2722
+ const parsed = parseSpec(text, file);
2723
+ const specKey = parsed.specId ?? file;
2724
+ const refs = parsed.contractRefs;
2725
+ if (refs.lines.length === 0) {
2726
+ missingRefSpecs.add(specKey);
2727
+ }
2728
+ const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
2729
+ for (const id of refs.ids) {
2730
+ currentContracts.add(id);
2731
+ const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
2732
+ specs.add(specKey);
2733
+ idToSpecs.set(id, specs);
2734
+ }
2735
+ specToContractIds.set(specKey, currentContracts);
2736
+ }
2737
+ return {
2738
+ specToContractIds,
2739
+ idToSpecs,
2740
+ missingRefSpecs
2741
+ };
2742
+ }
2360
2743
  async function collectIds(files) {
2361
2744
  const result = {
2362
2745
  SPEC: /* @__PURE__ */ new Set(),
@@ -2364,7 +2747,7 @@ async function collectIds(files) {
2364
2747
  SC: /* @__PURE__ */ new Set(),
2365
2748
  UI: /* @__PURE__ */ new Set(),
2366
2749
  API: /* @__PURE__ */ new Set(),
2367
- DATA: /* @__PURE__ */ new Set()
2750
+ DB: /* @__PURE__ */ new Set()
2368
2751
  };
2369
2752
  for (const file of files) {
2370
2753
  const text = await readFile11(file, "utf-8");
@@ -2379,7 +2762,7 @@ async function collectIds(files) {
2379
2762
  SC: toSortedArray2(result.SC),
2380
2763
  UI: toSortedArray2(result.UI),
2381
2764
  API: toSortedArray2(result.API),
2382
- DATA: toSortedArray2(result.DATA)
2765
+ DB: toSortedArray2(result.DB)
2383
2766
  };
2384
2767
  }
2385
2768
  async function collectUpstreamIds(files) {
@@ -2423,9 +2806,22 @@ function formatIdLine(label, values) {
2423
2806
  }
2424
2807
  return `- ${label}: ${values.join(", ")}`;
2425
2808
  }
2809
+ function formatList(values) {
2810
+ if (values.length === 0) {
2811
+ return "(none)";
2812
+ }
2813
+ return values.join(", ");
2814
+ }
2426
2815
  function toSortedArray2(values) {
2427
2816
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2428
2817
  }
2818
+ function mapToSortedRecord(values) {
2819
+ const record2 = {};
2820
+ for (const [key, files] of values.entries()) {
2821
+ record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
2822
+ }
2823
+ return record2;
2824
+ }
2429
2825
  function buildHotspots(issues) {
2430
2826
  const map = /* @__PURE__ */ new Map();
2431
2827
  for (const issue7 of issues) {