qfai 0.2.5 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +1 -1
  2. package/assets/init/.qfai/README.md +42 -0
  3. package/assets/init/.qfai/contracts/README.md +61 -0
  4. package/assets/init/{qfai → .qfai}/contracts/api/api-0001-sample.yaml +3 -0
  5. package/assets/init/{qfai → .qfai}/contracts/db/db-0001-sample.sql +3 -2
  6. package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +6 -0
  7. package/assets/init/.qfai/out/README.md +17 -0
  8. package/assets/init/.qfai/prompts/README.md +32 -0
  9. package/assets/init/{qfai → .qfai}/prompts/makeBusinessFlow.md +1 -1
  10. package/assets/init/{qfai → .qfai}/prompts/makeOverview.md +1 -1
  11. package/assets/init/.qfai/spec/README.md +80 -0
  12. package/assets/init/.qfai/spec/decisions/ADR-0001.md +9 -0
  13. package/assets/init/.qfai/spec/decisions/README.md +36 -0
  14. package/assets/init/.qfai/spec/scenarios/scenarios.feature +6 -0
  15. package/assets/init/.qfai/spec/spec-0001-sample.md +36 -0
  16. package/assets/init/root/qfai.config.yaml +8 -8
  17. package/dist/cli/index.cjs +498 -206
  18. package/dist/cli/index.cjs.map +1 -1
  19. package/dist/cli/index.d.ts +0 -2
  20. package/dist/cli/index.mjs +495 -203
  21. package/dist/cli/index.mjs.map +1 -1
  22. package/dist/index.cjs +471 -177
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +7 -4
  25. package/dist/index.d.ts +135 -2
  26. package/dist/index.mjs +470 -177
  27. package/dist/index.mjs.map +1 -1
  28. package/package.json +1 -1
  29. package/assets/init/qfai/README.md +0 -6
  30. package/assets/init/qfai/contracts/ui/ui-0001-sample.yaml +0 -4
  31. package/assets/init/qfai/spec/decisions/ADR-0001.md +0 -7
  32. package/assets/init/qfai/spec/scenarios.feature +0 -6
  33. package/assets/init/qfai/spec/spec-0001-sample.md +0 -29
  34. package/dist/cli/commands/init.d.ts +0 -8
  35. package/dist/cli/commands/init.d.ts.map +0 -1
  36. package/dist/cli/commands/init.js +0 -30
  37. package/dist/cli/commands/init.js.map +0 -1
  38. package/dist/cli/commands/report.d.ts +0 -8
  39. package/dist/cli/commands/report.d.ts.map +0 -1
  40. package/dist/cli/commands/report.js +0 -83
  41. package/dist/cli/commands/report.js.map +0 -1
  42. package/dist/cli/commands/validate.d.ts +0 -10
  43. package/dist/cli/commands/validate.d.ts.map +0 -1
  44. package/dist/cli/commands/validate.js +0 -66
  45. package/dist/cli/commands/validate.js.map +0 -1
  46. package/dist/cli/index.d.ts.map +0 -1
  47. package/dist/cli/index.js +0 -7
  48. package/dist/cli/index.js.map +0 -1
  49. package/dist/cli/lib/args.d.ts +0 -19
  50. package/dist/cli/lib/args.d.ts.map +0 -1
  51. package/dist/cli/lib/args.js +0 -107
  52. package/dist/cli/lib/args.js.map +0 -1
  53. package/dist/cli/lib/assets.d.ts +0 -2
  54. package/dist/cli/lib/assets.d.ts.map +0 -1
  55. package/dist/cli/lib/assets.js +0 -8
  56. package/dist/cli/lib/assets.js.map +0 -1
  57. package/dist/cli/lib/failOn.d.ts +0 -5
  58. package/dist/cli/lib/failOn.d.ts.map +0 -1
  59. package/dist/cli/lib/failOn.js +0 -10
  60. package/dist/cli/lib/failOn.js.map +0 -1
  61. package/dist/cli/lib/fs.d.ts +0 -11
  62. package/dist/cli/lib/fs.d.ts.map +0 -1
  63. package/dist/cli/lib/fs.js +0 -91
  64. package/dist/cli/lib/fs.js.map +0 -1
  65. package/dist/cli/lib/logger.d.ts +0 -4
  66. package/dist/cli/lib/logger.d.ts.map +0 -1
  67. package/dist/cli/lib/logger.js +0 -10
  68. package/dist/cli/lib/logger.js.map +0 -1
  69. package/dist/cli/main.d.ts +0 -2
  70. package/dist/cli/main.d.ts.map +0 -1
  71. package/dist/cli/main.js +0 -73
  72. package/dist/cli/main.js.map +0 -1
  73. package/dist/core/config.d.ts +0 -46
  74. package/dist/core/config.d.ts.map +0 -1
  75. package/dist/core/config.js +0 -224
  76. package/dist/core/config.js.map +0 -1
  77. package/dist/core/discovery.d.ts +0 -11
  78. package/dist/core/discovery.d.ts.map +0 -1
  79. package/dist/core/discovery.js +0 -31
  80. package/dist/core/discovery.js.map +0 -1
  81. package/dist/core/fs.d.ts +0 -6
  82. package/dist/core/fs.d.ts.map +0 -1
  83. package/dist/core/fs.js +0 -55
  84. package/dist/core/fs.js.map +0 -1
  85. package/dist/core/ids.d.ts +0 -5
  86. package/dist/core/ids.d.ts.map +0 -1
  87. package/dist/core/ids.js +0 -49
  88. package/dist/core/ids.js.map +0 -1
  89. package/dist/core/index.d.ts +0 -11
  90. package/dist/core/index.d.ts.map +0 -1
  91. package/dist/core/index.js +0 -11
  92. package/dist/core/index.js.map +0 -1
  93. package/dist/core/report.d.ts +0 -41
  94. package/dist/core/report.d.ts.map +0 -1
  95. package/dist/core/report.js +0 -238
  96. package/dist/core/report.js.map +0 -1
  97. package/dist/core/types.d.ts +0 -27
  98. package/dist/core/types.d.ts.map +0 -1
  99. package/dist/core/types.js +0 -2
  100. package/dist/core/types.js.map +0 -1
  101. package/dist/core/validate.d.ts +0 -4
  102. package/dist/core/validate.d.ts.map +0 -1
  103. package/dist/core/validate.js +0 -32
  104. package/dist/core/validate.js.map +0 -1
  105. package/dist/core/validators/contracts.d.ts +0 -5
  106. package/dist/core/validators/contracts.d.ts.map +0 -1
  107. package/dist/core/validators/contracts.js +0 -157
  108. package/dist/core/validators/contracts.js.map +0 -1
  109. package/dist/core/validators/scenario.d.ts +0 -5
  110. package/dist/core/validators/scenario.d.ts.map +0 -1
  111. package/dist/core/validators/scenario.js +0 -82
  112. package/dist/core/validators/scenario.js.map +0 -1
  113. package/dist/core/validators/spec.d.ts +0 -5
  114. package/dist/core/validators/spec.d.ts.map +0 -1
  115. package/dist/core/validators/spec.js +0 -69
  116. package/dist/core/validators/spec.js.map +0 -1
  117. package/dist/core/validators/traceability.d.ts +0 -4
  118. package/dist/core/validators/traceability.d.ts.map +0 -1
  119. package/dist/core/validators/traceability.js +0 -148
  120. package/dist/core/validators/traceability.js.map +0 -1
  121. package/dist/core/version.d.ts +0 -2
  122. package/dist/core/version.d.ts.map +0 -1
  123. package/dist/core/version.js +0 -25
  124. package/dist/core/version.js.map +0 -1
  125. package/dist/index.d.ts.map +0 -1
  126. package/dist/index.js +0 -2
  127. package/dist/index.js.map +0 -1
  128. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -108,9 +108,9 @@ function error(message) {
108
108
  async function runInit(options) {
109
109
  const assetsRoot = getInitAssetsDir();
110
110
  const rootAssets = path3.join(assetsRoot, "root");
111
- const qfaiAssets = path3.join(assetsRoot, "qfai");
111
+ const qfaiAssets = path3.join(assetsRoot, ".qfai");
112
112
  const destRoot = path3.resolve(options.dir);
113
- const destQfai = path3.join(destRoot, "qfai");
113
+ const destQfai = path3.join(destRoot, ".qfai");
114
114
  const rootResult = await copyTemplateTree(rootAssets, destRoot, {
115
115
  force: options.force,
116
116
  dryRun: options.dryRun
@@ -137,8 +137,8 @@ function report(copied, skipped, dryRun, label) {
137
137
  }
138
138
 
139
139
  // src/cli/commands/report.ts
140
- import { mkdir as mkdir2, readFile as readFile8, writeFile } from "fs/promises";
141
- import path9 from "path";
140
+ import { mkdir as mkdir2, readFile as readFile10, writeFile } from "fs/promises";
141
+ import path10 from "path";
142
142
 
143
143
  // src/core/config.ts
144
144
  import { readFile } from "fs/promises";
@@ -146,14 +146,13 @@ import path4 from "path";
146
146
  import { parse as parseYaml } from "yaml";
147
147
  var defaultConfig = {
148
148
  paths: {
149
- specDir: "qfai/spec",
150
- decisionsDir: "qfai/spec/decisions",
151
- scenariosDir: "qfai/spec",
152
- rulesDir: "qfai/rules",
153
- contractsDir: "qfai/contracts",
154
- uiContractsDir: "qfai/contracts/ui",
155
- apiContractsDir: "qfai/contracts/api",
156
- dataContractsDir: "qfai/contracts/db",
149
+ specDir: ".qfai/spec",
150
+ decisionsDir: ".qfai/spec/decisions",
151
+ scenariosDir: ".qfai/spec/scenarios",
152
+ contractsDir: ".qfai/contracts",
153
+ uiContractsDir: ".qfai/contracts/ui",
154
+ apiContractsDir: ".qfai/contracts/api",
155
+ dataContractsDir: ".qfai/contracts/db",
157
156
  srcDir: "src",
158
157
  testsDir: "tests"
159
158
  },
@@ -173,7 +172,8 @@ var defaultConfig = {
173
172
  traceability: {
174
173
  brMustHaveSc: true,
175
174
  scMustTouchContracts: true,
176
- allowOrphanContracts: false
175
+ allowOrphanContracts: false,
176
+ unknownContractIdSeverity: "error"
177
177
  }
178
178
  },
179
179
  output: {
@@ -248,13 +248,6 @@ function normalizePaths(raw, configPath, issues) {
248
248
  configPath,
249
249
  issues
250
250
  ),
251
- rulesDir: readString(
252
- raw.rulesDir,
253
- base.rulesDir,
254
- "paths.rulesDir",
255
- configPath,
256
- issues
257
- ),
258
251
  contractsDir: readString(
259
252
  raw.contractsDir,
260
253
  base.contractsDir,
@@ -379,6 +372,13 @@ function normalizeValidation(raw, configPath, issues) {
379
372
  "validation.traceability.allowOrphanContracts",
380
373
  configPath,
381
374
  issues
375
+ ),
376
+ unknownContractIdSeverity: readTraceabilitySeverity(
377
+ traceabilityRaw?.unknownContractIdSeverity,
378
+ base.traceability.unknownContractIdSeverity,
379
+ "validation.traceability.unknownContractIdSeverity",
380
+ configPath,
381
+ issues
382
382
  )
383
383
  }
384
384
  };
@@ -458,6 +458,20 @@ function readFailOn(value, fallback, label, configPath, issues) {
458
458
  }
459
459
  return fallback;
460
460
  }
461
+ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
462
+ if (value === "warning" || value === "error") {
463
+ return value;
464
+ }
465
+ if (value !== void 0) {
466
+ issues.push(
467
+ configIssue(
468
+ configPath,
469
+ `${label} \u306F warning|error \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
470
+ )
471
+ );
472
+ }
473
+ return fallback;
474
+ }
461
475
  function readOutputFormat(value, fallback, label, configPath, issues) {
462
476
  if (value === "text" || value === "json" || value === "github") {
463
477
  return value;
@@ -498,7 +512,7 @@ function isRecord(value) {
498
512
  }
499
513
 
500
514
  // src/core/report.ts
501
- import { readFile as readFile7 } from "fs/promises";
515
+ import { readFile as readFile9 } from "fs/promises";
502
516
 
503
517
  // src/core/discovery.ts
504
518
  import path6 from "path";
@@ -559,8 +573,7 @@ async function exists2(target) {
559
573
  }
560
574
 
561
575
  // src/core/discovery.ts
562
- var LEGACY_SPEC_NAME = "spec.md";
563
- var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/i;
576
+ var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/;
564
577
  async function collectSpecFiles(specRoot) {
565
578
  const files = await collectFiles(specRoot, { extensions: [".md"] });
566
579
  return files.filter((file) => isSpecFile(file));
@@ -584,17 +597,19 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
584
597
  }
585
598
  function isSpecFile(filePath) {
586
599
  const name = path6.basename(filePath).toLowerCase();
587
- return name === LEGACY_SPEC_NAME || SPEC_NAMED_PATTERN.test(name);
600
+ return SPEC_NAMED_PATTERN.test(name);
588
601
  }
589
602
 
590
603
  // src/core/ids.ts
591
- var ID_PATTERNS = {
592
- SPEC: /\bSPEC-[A-Z0-9-]+\b/g,
593
- BR: /\bBR-[A-Z0-9-]+\b/g,
594
- SC: /\bSC-[A-Z0-9-]+\b/g,
595
- UI: /\bUI-[A-Z0-9-]+\b/g,
596
- API: /\bAPI-[A-Z0-9-]+\b/g,
597
- DATA: /\bDATA-[A-Z0-9-]+\b/g
604
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
605
+ var STRICT_ID_PATTERNS = {
606
+ SPEC: /\bSPEC-\d{4}\b/g,
607
+ BR: /\bBR-\d{4}\b/g,
608
+ SC: /\bSC-\d{4}\b/g,
609
+ UI: /\bUI-\d{4}\b/g,
610
+ API: /\bAPI-\d{4}\b/g,
611
+ DATA: /\bDATA-\d{4}\b/g,
612
+ ADR: /\bADR-\d{4}\b/g
598
613
  };
599
614
  var LOOSE_ID_PATTERNS = {
600
615
  SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
@@ -602,16 +617,17 @@ var LOOSE_ID_PATTERNS = {
602
617
  SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
603
618
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
604
619
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
605
- DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi
620
+ DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi,
621
+ ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
606
622
  };
607
623
  function extractIds(text, prefix) {
608
- const pattern = ID_PATTERNS[prefix];
624
+ const pattern = STRICT_ID_PATTERNS[prefix];
609
625
  const matches = text.match(pattern);
610
626
  return unique(matches ?? []);
611
627
  }
612
628
  function extractAllIds(text) {
613
629
  const all = [];
614
- Object.keys(ID_PATTERNS).forEach((prefix) => {
630
+ ID_PREFIXES.forEach((prefix) => {
615
631
  all.push(...extractIds(text, prefix));
616
632
  });
617
633
  return unique(all);
@@ -632,7 +648,7 @@ function unique(values) {
632
648
  return Array.from(new Set(values));
633
649
  }
634
650
  function isValidId(value, prefix) {
635
- const pattern = ID_PATTERNS[prefix];
651
+ const pattern = STRICT_ID_PATTERNS[prefix];
636
652
  const strict = new RegExp(pattern.source);
637
653
  return strict.test(value);
638
654
  }
@@ -645,8 +661,8 @@ import { readFile as readFile2 } from "fs/promises";
645
661
  import path7 from "path";
646
662
  import { fileURLToPath as fileURLToPath2 } from "url";
647
663
  async function resolveToolVersion() {
648
- if ("0.2.5".length > 0) {
649
- return "0.2.5";
664
+ if ("0.2.9".length > 0) {
665
+ return "0.2.9";
650
666
  }
651
667
  try {
652
668
  const packagePath = resolvePackageJsonPath();
@@ -666,8 +682,50 @@ function resolvePackageJsonPath() {
666
682
 
667
683
  // src/core/validators/contracts.ts
668
684
  import { readFile as readFile3 } from "fs/promises";
685
+
686
+ // src/core/contracts.ts
669
687
  import path8 from "path";
670
688
  import { parse as parseYaml2 } from "yaml";
689
+ function parseStructuredContract(file, text) {
690
+ const ext = path8.extname(file).toLowerCase();
691
+ if (ext === ".json") {
692
+ return JSON.parse(text);
693
+ }
694
+ return parseYaml2(text);
695
+ }
696
+ function extractUiContractIds(doc) {
697
+ const id = typeof doc.id === "string" ? doc.id : "";
698
+ return extractIds(id, "UI");
699
+ }
700
+ function extractApiContractIds(doc) {
701
+ const operationIds = /* @__PURE__ */ new Set();
702
+ collectOperationIds(doc, operationIds);
703
+ const ids = /* @__PURE__ */ new Set();
704
+ for (const operationId of operationIds) {
705
+ extractIds(operationId, "API").forEach((id) => ids.add(id));
706
+ }
707
+ return Array.from(ids);
708
+ }
709
+ function collectOperationIds(value, out) {
710
+ if (!value || typeof value !== "object") {
711
+ return;
712
+ }
713
+ if (Array.isArray(value)) {
714
+ for (const item of value) {
715
+ collectOperationIds(item, out);
716
+ }
717
+ return;
718
+ }
719
+ for (const [key, entry] of Object.entries(value)) {
720
+ if (key === "operationId" && typeof entry === "string") {
721
+ out.add(entry);
722
+ continue;
723
+ }
724
+ collectOperationIds(entry, out);
725
+ }
726
+ }
727
+
728
+ // src/core/validators/contracts.ts
671
729
  var SQL_DANGEROUS_PATTERNS = [
672
730
  { pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
673
731
  { pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
@@ -716,12 +774,13 @@ async function validateUiContracts(uiRoot) {
716
774
  "SC",
717
775
  "UI",
718
776
  "API",
719
- "DATA"
777
+ "DATA",
778
+ "ADR"
720
779
  ]);
721
780
  if (invalidIds.length > 0) {
722
781
  issues.push(
723
782
  issue(
724
- "QFAI_ID_INVALID_FORMAT",
783
+ "QFAI-ID-002",
725
784
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
726
785
  "error",
727
786
  file,
@@ -730,30 +789,32 @@ async function validateUiContracts(uiRoot) {
730
789
  )
731
790
  );
732
791
  }
792
+ let doc;
733
793
  try {
734
- const doc = parseYaml2(text);
735
- const id = typeof doc.id === "string" ? doc.id : "";
736
- if (!id.startsWith("UI-")) {
737
- issues.push(
738
- issue(
739
- "QFAI-UI-001",
740
- "UI \u5951\u7D04\u306E id \u306F UI- \u3067\u59CB\u307E\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
741
- "error",
742
- file,
743
- "contracts.ui.id"
744
- )
745
- );
746
- }
794
+ doc = parseStructuredContract(file, text);
747
795
  } catch (error2) {
748
796
  issues.push(
749
797
  issue(
750
- "QFAI-UI-002",
751
- `UI YAML \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError2(error2)}`,
798
+ "QFAI-CONTRACT-001",
799
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
752
800
  "error",
753
801
  file,
754
802
  "contracts.ui.parse"
755
803
  )
756
804
  );
805
+ continue;
806
+ }
807
+ const uiIds = extractUiContractIds(doc);
808
+ if (uiIds.length === 0) {
809
+ issues.push(
810
+ issue(
811
+ "QFAI-CONTRACT-002",
812
+ `UI \u5951\u7D04\u306B ID(UI-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
813
+ "error",
814
+ file,
815
+ "contracts.ui.id"
816
+ )
817
+ );
757
818
  }
758
819
  }
759
820
  return issues;
@@ -780,12 +841,13 @@ async function validateApiContracts(apiRoot) {
780
841
  "SC",
781
842
  "UI",
782
843
  "API",
783
- "DATA"
844
+ "DATA",
845
+ "ADR"
784
846
  ]);
785
847
  if (invalidIds.length > 0) {
786
848
  issues.push(
787
849
  issue(
788
- "QFAI_ID_INVALID_FORMAT",
850
+ "QFAI-ID-002",
789
851
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
790
852
  "error",
791
853
  file,
@@ -794,29 +856,43 @@ async function validateApiContracts(apiRoot) {
794
856
  )
795
857
  );
796
858
  }
859
+ let doc;
797
860
  try {
798
- const doc = parseStructured(file, text);
799
- if (!doc || !hasOpenApi(doc)) {
800
- issues.push(
801
- issue(
802
- "QFAI-API-001",
803
- "OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
804
- "error",
805
- file,
806
- "contracts.api.openapi"
807
- )
808
- );
809
- }
861
+ doc = parseStructuredContract(file, text);
810
862
  } catch (error2) {
811
863
  issues.push(
812
864
  issue(
813
- "QFAI-API-002",
814
- `API \u5B9A\u7FA9\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError2(error2)}`,
865
+ "QFAI-CONTRACT-001",
866
+ `API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError2(error2)})`,
815
867
  "error",
816
868
  file,
817
869
  "contracts.api.parse"
818
870
  )
819
871
  );
872
+ continue;
873
+ }
874
+ if (!hasOpenApi(doc)) {
875
+ issues.push(
876
+ issue(
877
+ "QFAI-API-001",
878
+ "OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
879
+ "error",
880
+ file,
881
+ "contracts.api.openapi"
882
+ )
883
+ );
884
+ }
885
+ const apiIds = extractApiContractIds(doc);
886
+ if (apiIds.length === 0) {
887
+ issues.push(
888
+ issue(
889
+ "QFAI-CONTRACT-002",
890
+ `API \u5951\u7D04\u306B ID(API-xxxx) \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}`,
891
+ "error",
892
+ file,
893
+ "contracts.api.id"
894
+ )
895
+ );
820
896
  }
821
897
  }
822
898
  return issues;
@@ -843,12 +919,13 @@ async function validateDataContracts(dataRoot) {
843
919
  "SC",
844
920
  "UI",
845
921
  "API",
846
- "DATA"
922
+ "DATA",
923
+ "ADR"
847
924
  ]);
848
925
  if (invalidIds.length > 0) {
849
926
  issues.push(
850
927
  issue(
851
- "QFAI_ID_INVALID_FORMAT",
928
+ "QFAI-ID-002",
852
929
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
853
930
  "error",
854
931
  file,
@@ -878,13 +955,6 @@ function lintSql(text, file) {
878
955
  }
879
956
  return issues;
880
957
  }
881
- function parseStructured(file, text) {
882
- const ext = path8.extname(file).toLowerCase();
883
- if (ext === ".json") {
884
- return JSON.parse(text);
885
- }
886
- return parseYaml2(text);
887
- }
888
958
  function hasOpenApi(doc) {
889
959
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
890
960
  }
@@ -895,25 +965,165 @@ function formatError2(error2) {
895
965
  return String(error2);
896
966
  }
897
967
  function issue(code, message, severity, file, rule, refs) {
898
- const issue5 = {
968
+ const issue6 = {
899
969
  code,
900
970
  severity,
901
971
  message
902
972
  };
903
973
  if (file) {
904
- issue5.file = file;
974
+ issue6.file = file;
905
975
  }
906
976
  if (rule) {
907
- issue5.rule = rule;
977
+ issue6.rule = rule;
908
978
  }
909
979
  if (refs && refs.length > 0) {
910
- issue5.refs = refs;
980
+ issue6.refs = refs;
911
981
  }
912
- return issue5;
982
+ return issue6;
913
983
  }
914
984
 
915
- // src/core/validators/scenario.ts
985
+ // src/core/validators/ids.ts
986
+ import { readFile as readFile5 } from "fs/promises";
987
+ import path9 from "path";
988
+
989
+ // src/core/contractIndex.ts
916
990
  import { readFile as readFile4 } from "fs/promises";
991
+ async function buildContractIndex(root, config) {
992
+ const uiRoot = resolvePath(root, config, "uiContractsDir");
993
+ const apiRoot = resolvePath(root, config, "apiContractsDir");
994
+ const dataRoot = resolvePath(root, config, "dataContractsDir");
995
+ const [uiFiles, apiFiles, dataFiles] = await Promise.all([
996
+ collectUiContractFiles(uiRoot),
997
+ collectApiContractFiles(apiRoot),
998
+ collectDataContractFiles(dataRoot)
999
+ ]);
1000
+ const index = {
1001
+ ids: /* @__PURE__ */ new Set(),
1002
+ idToFiles: /* @__PURE__ */ new Map(),
1003
+ files: { ui: uiFiles, api: apiFiles, data: dataFiles },
1004
+ structuredParseFailedFiles: /* @__PURE__ */ new Set()
1005
+ };
1006
+ await indexUiContracts(uiFiles, index);
1007
+ await indexApiContracts(apiFiles, index);
1008
+ await indexDataContracts(dataFiles, index);
1009
+ return index;
1010
+ }
1011
+ async function indexUiContracts(files, index) {
1012
+ for (const file of files) {
1013
+ const text = await readFile4(file, "utf-8");
1014
+ try {
1015
+ const doc = parseStructuredContract(file, text);
1016
+ extractUiContractIds(doc).forEach((id) => record(index, id, file));
1017
+ } catch {
1018
+ index.structuredParseFailedFiles.add(file);
1019
+ extractIds(text, "UI").forEach((id) => record(index, id, file));
1020
+ }
1021
+ }
1022
+ }
1023
+ async function indexApiContracts(files, index) {
1024
+ for (const file of files) {
1025
+ const text = await readFile4(file, "utf-8");
1026
+ try {
1027
+ const doc = parseStructuredContract(file, text);
1028
+ extractApiContractIds(doc).forEach((id) => record(index, id, file));
1029
+ } catch {
1030
+ index.structuredParseFailedFiles.add(file);
1031
+ extractIds(text, "API").forEach((id) => record(index, id, file));
1032
+ }
1033
+ }
1034
+ }
1035
+ async function indexDataContracts(files, index) {
1036
+ for (const file of files) {
1037
+ const text = await readFile4(file, "utf-8");
1038
+ extractIds(text, "DATA").forEach((id) => record(index, id, file));
1039
+ }
1040
+ }
1041
+ function record(index, id, file) {
1042
+ index.ids.add(id);
1043
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
1044
+ current.add(file);
1045
+ index.idToFiles.set(id, current);
1046
+ }
1047
+
1048
+ // src/core/validators/ids.ts
1049
+ async function validateDefinedIds(root, config) {
1050
+ const issues = [];
1051
+ const specRoot = resolvePath(root, config, "specDir");
1052
+ const scenarioRoot = resolvePath(root, config, "scenariosDir");
1053
+ const specFiles = await collectSpecFiles(specRoot);
1054
+ const scenarioFiles = await collectFiles(scenarioRoot, {
1055
+ extensions: [".feature"]
1056
+ });
1057
+ const defined = /* @__PURE__ */ new Map();
1058
+ await collectSpecDefinitionIds(specFiles, defined);
1059
+ await collectScenarioDefinitionIds(scenarioFiles, defined);
1060
+ const contractIndex = await buildContractIndex(root, config);
1061
+ for (const [id, files] of contractIndex.idToFiles.entries()) {
1062
+ for (const file of files) {
1063
+ recordId(defined, id, file);
1064
+ }
1065
+ }
1066
+ for (const [id, files] of defined.entries()) {
1067
+ if (files.size <= 1) {
1068
+ continue;
1069
+ }
1070
+ const sorted = Array.from(files).sort();
1071
+ issues.push(
1072
+ issue2(
1073
+ "QFAI-ID-001",
1074
+ `ID \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${id} (${formatFileList(sorted, root)})`,
1075
+ "error",
1076
+ sorted[0],
1077
+ "id.duplicate"
1078
+ )
1079
+ );
1080
+ }
1081
+ return issues;
1082
+ }
1083
+ async function collectSpecDefinitionIds(files, out) {
1084
+ for (const file of files) {
1085
+ const text = await readFile5(file, "utf-8");
1086
+ extractIds(text, "SPEC").forEach((id) => recordId(out, id, file));
1087
+ extractIds(text, "BR").forEach((id) => recordId(out, id, file));
1088
+ }
1089
+ }
1090
+ async function collectScenarioDefinitionIds(files, out) {
1091
+ for (const file of files) {
1092
+ const text = await readFile5(file, "utf-8");
1093
+ extractIds(text, "SC").forEach((id) => recordId(out, id, file));
1094
+ }
1095
+ }
1096
+ function recordId(out, id, file) {
1097
+ const current = out.get(id) ?? /* @__PURE__ */ new Set();
1098
+ current.add(file);
1099
+ out.set(id, current);
1100
+ }
1101
+ function formatFileList(files, root) {
1102
+ return files.map((file) => {
1103
+ const relative = path9.relative(root, file);
1104
+ return relative.length > 0 ? relative : file;
1105
+ }).join(", ");
1106
+ }
1107
+ function issue2(code, message, severity, file, rule, refs) {
1108
+ const issue6 = {
1109
+ code,
1110
+ severity,
1111
+ message
1112
+ };
1113
+ if (file) {
1114
+ issue6.file = file;
1115
+ }
1116
+ if (rule) {
1117
+ issue6.rule = rule;
1118
+ }
1119
+ if (refs && refs.length > 0) {
1120
+ issue6.refs = refs;
1121
+ }
1122
+ return issue6;
1123
+ }
1124
+
1125
+ // src/core/validators/scenario.ts
1126
+ import { readFile as readFile6 } from "fs/promises";
917
1127
  var GIVEN_PATTERN = /\bGiven\b/;
918
1128
  var WHEN_PATTERN = /\bWhen\b/;
919
1129
  var THEN_PATTERN = /\bThen\b/;
@@ -924,7 +1134,7 @@ async function validateScenarios(root, config) {
924
1134
  });
925
1135
  if (files.length === 0) {
926
1136
  return [
927
- issue2(
1137
+ issue3(
928
1138
  "QFAI-SC-000",
929
1139
  "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
930
1140
  "info",
@@ -935,7 +1145,7 @@ async function validateScenarios(root, config) {
935
1145
  }
936
1146
  const issues = [];
937
1147
  for (const file of files) {
938
- const text = await readFile4(file, "utf-8");
1148
+ const text = await readFile6(file, "utf-8");
939
1149
  issues.push(...validateScenarioContent(text, file));
940
1150
  }
941
1151
  return issues;
@@ -948,12 +1158,13 @@ function validateScenarioContent(text, file) {
948
1158
  "SC",
949
1159
  "UI",
950
1160
  "API",
951
- "DATA"
1161
+ "DATA",
1162
+ "ADR"
952
1163
  ]);
953
1164
  if (invalidIds.length > 0) {
954
1165
  issues.push(
955
- issue2(
956
- "QFAI_ID_INVALID_FORMAT",
1166
+ issue3(
1167
+ "QFAI-ID-002",
957
1168
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
958
1169
  "error",
959
1170
  file,
@@ -965,7 +1176,7 @@ function validateScenarioContent(text, file) {
965
1176
  const scIds = extractIds(text, "SC");
966
1177
  if (scIds.length === 0) {
967
1178
  issues.push(
968
- issue2(
1179
+ issue3(
969
1180
  "QFAI-SC-001",
970
1181
  "SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
971
1182
  "error",
@@ -977,7 +1188,7 @@ function validateScenarioContent(text, file) {
977
1188
  const specIds = extractIds(text, "SPEC");
978
1189
  if (specIds.length === 0) {
979
1190
  issues.push(
980
- issue2(
1191
+ issue3(
981
1192
  "QFAI-SC-002",
982
1193
  "SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
983
1194
  "error",
@@ -989,7 +1200,7 @@ function validateScenarioContent(text, file) {
989
1200
  const brIds = extractIds(text, "BR");
990
1201
  if (brIds.length === 0) {
991
1202
  issues.push(
992
- issue2(
1203
+ issue3(
993
1204
  "QFAI-SC-003",
994
1205
  "SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
995
1206
  "error",
@@ -1010,7 +1221,7 @@ function validateScenarioContent(text, file) {
1010
1221
  }
1011
1222
  if (missingSteps.length > 0) {
1012
1223
  issues.push(
1013
- issue2(
1224
+ issue3(
1014
1225
  "QFAI-SC-005",
1015
1226
  `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1016
1227
  "warning",
@@ -1021,34 +1232,35 @@ function validateScenarioContent(text, file) {
1021
1232
  }
1022
1233
  return issues;
1023
1234
  }
1024
- function issue2(code, message, severity, file, rule, refs) {
1025
- const issue5 = {
1235
+ function issue3(code, message, severity, file, rule, refs) {
1236
+ const issue6 = {
1026
1237
  code,
1027
1238
  severity,
1028
1239
  message
1029
1240
  };
1030
1241
  if (file) {
1031
- issue5.file = file;
1242
+ issue6.file = file;
1032
1243
  }
1033
1244
  if (rule) {
1034
- issue5.rule = rule;
1245
+ issue6.rule = rule;
1035
1246
  }
1036
1247
  if (refs && refs.length > 0) {
1037
- issue5.refs = refs;
1248
+ issue6.refs = refs;
1038
1249
  }
1039
- return issue5;
1250
+ return issue6;
1040
1251
  }
1041
1252
 
1042
1253
  // src/core/validators/spec.ts
1043
- import { readFile as readFile5 } from "fs/promises";
1254
+ import { readFile as readFile7 } from "fs/promises";
1044
1255
  async function validateSpecs(root, config) {
1045
1256
  const specsRoot = resolvePath(root, config, "specDir");
1046
1257
  const files = await collectSpecFiles(specsRoot);
1047
1258
  if (files.length === 0) {
1259
+ const expected = "spec-0001-<slug>.md";
1048
1260
  return [
1049
- issue3(
1261
+ issue4(
1050
1262
  "QFAI-SPEC-000",
1051
- "Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1263
+ `Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
1052
1264
  "info",
1053
1265
  specsRoot,
1054
1266
  "spec.files"
@@ -1057,7 +1269,7 @@ async function validateSpecs(root, config) {
1057
1269
  }
1058
1270
  const issues = [];
1059
1271
  for (const file of files) {
1060
- const text = await readFile5(file, "utf-8");
1272
+ const text = await readFile7(file, "utf-8");
1061
1273
  issues.push(
1062
1274
  ...validateSpecContent(
1063
1275
  text,
@@ -1076,12 +1288,13 @@ function validateSpecContent(text, file, requiredSections) {
1076
1288
  "SC",
1077
1289
  "UI",
1078
1290
  "API",
1079
- "DATA"
1291
+ "DATA",
1292
+ "ADR"
1080
1293
  ]);
1081
1294
  if (invalidIds.length > 0) {
1082
1295
  issues.push(
1083
- issue3(
1084
- "QFAI_ID_INVALID_FORMAT",
1296
+ issue4(
1297
+ "QFAI-ID-002",
1085
1298
  `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1086
1299
  "error",
1087
1300
  file,
@@ -1093,7 +1306,7 @@ function validateSpecContent(text, file, requiredSections) {
1093
1306
  const specIds = extractIds(text, "SPEC");
1094
1307
  if (specIds.length === 0) {
1095
1308
  issues.push(
1096
- issue3(
1309
+ issue4(
1097
1310
  "QFAI-SPEC-001",
1098
1311
  "SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1099
1312
  "error",
@@ -1105,7 +1318,7 @@ function validateSpecContent(text, file, requiredSections) {
1105
1318
  const brIds = extractIds(text, "BR");
1106
1319
  if (brIds.length === 0) {
1107
1320
  issues.push(
1108
- issue3(
1321
+ issue4(
1109
1322
  "QFAI-SPEC-002",
1110
1323
  "BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1111
1324
  "error",
@@ -1117,7 +1330,7 @@ function validateSpecContent(text, file, requiredSections) {
1117
1330
  const scIds = extractIds(text, "SC");
1118
1331
  if (scIds.length > 0) {
1119
1332
  issues.push(
1120
- issue3(
1333
+ issue4(
1121
1334
  "QFAI-SPEC-003",
1122
1335
  "Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
1123
1336
  "warning",
@@ -1130,7 +1343,7 @@ function validateSpecContent(text, file, requiredSections) {
1130
1343
  for (const section of requiredSections) {
1131
1344
  if (!text.includes(section)) {
1132
1345
  issues.push(
1133
- issue3(
1346
+ issue4(
1134
1347
  "QFAI-SPEC-004",
1135
1348
  `\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
1136
1349
  "error",
@@ -1142,26 +1355,26 @@ function validateSpecContent(text, file, requiredSections) {
1142
1355
  }
1143
1356
  return issues;
1144
1357
  }
1145
- function issue3(code, message, severity, file, rule, refs) {
1146
- const issue5 = {
1358
+ function issue4(code, message, severity, file, rule, refs) {
1359
+ const issue6 = {
1147
1360
  code,
1148
1361
  severity,
1149
1362
  message
1150
1363
  };
1151
1364
  if (file) {
1152
- issue5.file = file;
1365
+ issue6.file = file;
1153
1366
  }
1154
1367
  if (rule) {
1155
- issue5.rule = rule;
1368
+ issue6.rule = rule;
1156
1369
  }
1157
1370
  if (refs && refs.length > 0) {
1158
- issue5.refs = refs;
1371
+ issue6.refs = refs;
1159
1372
  }
1160
- return issue5;
1373
+ return issue6;
1161
1374
  }
1162
1375
 
1163
1376
  // src/core/validators/traceability.ts
1164
- import { readFile as readFile6 } from "fs/promises";
1377
+ import { readFile as readFile8 } from "fs/promises";
1165
1378
  async function validateTraceability(root, config) {
1166
1379
  const issues = [];
1167
1380
  const specsRoot = resolvePath(root, config, "specDir");
@@ -1177,36 +1390,141 @@ async function validateTraceability(root, config) {
1177
1390
  extensions: [".feature"]
1178
1391
  });
1179
1392
  const upstreamIds = /* @__PURE__ */ new Set();
1393
+ const specIds = /* @__PURE__ */ new Set();
1180
1394
  const brIdsInSpecs = /* @__PURE__ */ new Set();
1181
1395
  const brIdsInScenarios = /* @__PURE__ */ new Set();
1182
1396
  const scIdsInScenarios = /* @__PURE__ */ new Set();
1183
1397
  const scenarioContractIds = /* @__PURE__ */ new Set();
1184
1398
  const scWithContracts = /* @__PURE__ */ new Set();
1185
- for (const file of [...specFiles, ...decisionFiles]) {
1186
- const text = await readFile6(file, "utf-8");
1399
+ const specToBrIds = /* @__PURE__ */ new Map();
1400
+ const contractIndex = await buildContractIndex(root, config);
1401
+ const contractIds = contractIndex.ids;
1402
+ for (const file of specFiles) {
1403
+ const text = await readFile8(file, "utf-8");
1404
+ extractAllIds(text).forEach((id) => upstreamIds.add(id));
1405
+ const specIdsInFile = extractIds(text, "SPEC");
1406
+ specIdsInFile.forEach((id) => specIds.add(id));
1407
+ const brIds = extractIds(text, "BR");
1408
+ brIds.forEach((id) => brIdsInSpecs.add(id));
1409
+ const referencedContractIds = /* @__PURE__ */ new Set([
1410
+ ...extractIds(text, "UI"),
1411
+ ...extractIds(text, "API"),
1412
+ ...extractIds(text, "DATA")
1413
+ ]);
1414
+ const unknownContractIds = Array.from(referencedContractIds).filter(
1415
+ (id) => !contractIds.has(id)
1416
+ );
1417
+ if (unknownContractIds.length > 0) {
1418
+ issues.push(
1419
+ issue5(
1420
+ "QFAI-TRACE-009",
1421
+ `Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1422
+ ", "
1423
+ )}`,
1424
+ "error",
1425
+ file,
1426
+ "traceability.specContractExists",
1427
+ unknownContractIds
1428
+ )
1429
+ );
1430
+ }
1431
+ for (const specId of specIdsInFile) {
1432
+ const current = specToBrIds.get(specId) ?? /* @__PURE__ */ new Set();
1433
+ brIds.forEach((id) => current.add(id));
1434
+ specToBrIds.set(specId, current);
1435
+ }
1436
+ }
1437
+ for (const file of decisionFiles) {
1438
+ const text = await readFile8(file, "utf-8");
1187
1439
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1188
- extractIds(text, "BR").forEach((id) => brIdsInSpecs.add(id));
1189
1440
  }
1190
1441
  for (const file of scenarioFiles) {
1191
- const text = await readFile6(file, "utf-8");
1442
+ const text = await readFile8(file, "utf-8");
1192
1443
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
1444
+ const specIdsInScenario = extractIds(text, "SPEC");
1193
1445
  const brIds = extractIds(text, "BR");
1194
- brIds.forEach((id) => brIdsInScenarios.add(id));
1195
1446
  const scIds = extractIds(text, "SC");
1196
- scIds.forEach((id) => scIdsInScenarios.add(id));
1197
- const contractIds = [
1447
+ const scenarioIds = [
1198
1448
  ...extractIds(text, "UI"),
1199
1449
  ...extractIds(text, "API"),
1200
1450
  ...extractIds(text, "DATA")
1201
1451
  ];
1202
- contractIds.forEach((id) => scenarioContractIds.add(id));
1203
- if (contractIds.length > 0) {
1452
+ brIds.forEach((id) => brIdsInScenarios.add(id));
1453
+ scIds.forEach((id) => scIdsInScenarios.add(id));
1454
+ scenarioIds.forEach((id) => scenarioContractIds.add(id));
1455
+ if (scenarioIds.length > 0) {
1204
1456
  scIds.forEach((id) => scWithContracts.add(id));
1205
1457
  }
1458
+ const unknownSpecIds = specIdsInScenario.filter((id) => !specIds.has(id));
1459
+ if (unknownSpecIds.length > 0) {
1460
+ issues.push(
1461
+ issue5(
1462
+ "QFAI-TRACE-005",
1463
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
1464
+ "error",
1465
+ file,
1466
+ "traceability.scenarioSpecExists",
1467
+ unknownSpecIds
1468
+ )
1469
+ );
1470
+ }
1471
+ const unknownBrIds = brIds.filter((id) => !brIdsInSpecs.has(id));
1472
+ if (unknownBrIds.length > 0) {
1473
+ issues.push(
1474
+ issue5(
1475
+ "QFAI-TRACE-006",
1476
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
1477
+ "error",
1478
+ file,
1479
+ "traceability.scenarioBrExists",
1480
+ unknownBrIds
1481
+ )
1482
+ );
1483
+ }
1484
+ const unknownContractIds = scenarioIds.filter((id) => !contractIds.has(id));
1485
+ if (unknownContractIds.length > 0) {
1486
+ issues.push(
1487
+ issue5(
1488
+ "QFAI-TRACE-008",
1489
+ `Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
1490
+ ", "
1491
+ )}`,
1492
+ config.validation.traceability.unknownContractIdSeverity,
1493
+ file,
1494
+ "traceability.scenarioContractExists",
1495
+ unknownContractIds
1496
+ )
1497
+ );
1498
+ }
1499
+ if (specIdsInScenario.length > 0) {
1500
+ const allowedBrIds = /* @__PURE__ */ new Set();
1501
+ for (const specId of specIdsInScenario) {
1502
+ const brIdsForSpec = specToBrIds.get(specId);
1503
+ if (!brIdsForSpec) {
1504
+ continue;
1505
+ }
1506
+ brIdsForSpec.forEach((id) => allowedBrIds.add(id));
1507
+ }
1508
+ const invalidBrIds = brIds.filter((id) => !allowedBrIds.has(id));
1509
+ if (invalidBrIds.length > 0) {
1510
+ issues.push(
1511
+ issue5(
1512
+ "QFAI-TRACE-007",
1513
+ `Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
1514
+ ", "
1515
+ )} (SPEC: ${specIdsInScenario.join(", ")})`,
1516
+ "error",
1517
+ file,
1518
+ "traceability.scenarioBrUnderSpec",
1519
+ invalidBrIds
1520
+ )
1521
+ );
1522
+ }
1523
+ }
1206
1524
  }
1207
1525
  if (upstreamIds.size === 0) {
1208
1526
  return [
1209
- issue4(
1527
+ issue5(
1210
1528
  "QFAI-TRACE-000",
1211
1529
  "\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1212
1530
  "info",
@@ -1221,7 +1539,7 @@ async function validateTraceability(root, config) {
1221
1539
  );
1222
1540
  if (orphanBrIds.length > 0) {
1223
1541
  issues.push(
1224
- issue4(
1542
+ issue5(
1225
1543
  "QFAI_TRACE_BR_ORPHAN",
1226
1544
  `BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
1227
1545
  "error",
@@ -1238,7 +1556,7 @@ async function validateTraceability(root, config) {
1238
1556
  );
1239
1557
  if (scWithoutContracts.length > 0) {
1240
1558
  issues.push(
1241
- issue4(
1559
+ issue5(
1242
1560
  "QFAI_TRACE_SC_NO_CONTRACT",
1243
1561
  `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
1244
1562
  ", "
@@ -1252,14 +1570,13 @@ async function validateTraceability(root, config) {
1252
1570
  }
1253
1571
  }
1254
1572
  if (!config.validation.traceability.allowOrphanContracts) {
1255
- const contractIds = await collectContractIds(root, config);
1256
1573
  if (contractIds.size > 0) {
1257
1574
  const orphanContracts = Array.from(contractIds).filter(
1258
1575
  (id) => !scenarioContractIds.has(id)
1259
1576
  );
1260
1577
  if (orphanContracts.length > 0) {
1261
1578
  issues.push(
1262
- issue4(
1579
+ issue5(
1263
1580
  "QFAI_CONTRACT_ORPHAN",
1264
1581
  `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1265
1582
  "error",
@@ -1276,27 +1593,6 @@ async function validateTraceability(root, config) {
1276
1593
  );
1277
1594
  return issues;
1278
1595
  }
1279
- async function collectContractIds(root, config) {
1280
- const contractIds = /* @__PURE__ */ new Set();
1281
- const uiRoot = resolvePath(root, config, "uiContractsDir");
1282
- const apiRoot = resolvePath(root, config, "apiContractsDir");
1283
- const dataRoot = resolvePath(root, config, "dataContractsDir");
1284
- const uiFiles = await collectUiContractFiles(uiRoot);
1285
- const apiFiles = await collectApiContractFiles(apiRoot);
1286
- const dataFiles = await collectDataContractFiles(dataRoot);
1287
- await collectIdsFromFiles(uiFiles, ["UI"], contractIds);
1288
- await collectIdsFromFiles(apiFiles, ["API"], contractIds);
1289
- await collectIdsFromFiles(dataFiles, ["DATA"], contractIds);
1290
- return contractIds;
1291
- }
1292
- async function collectIdsFromFiles(files, prefixes, out) {
1293
- for (const file of files) {
1294
- const text = await readFile6(file, "utf-8");
1295
- for (const prefix of prefixes) {
1296
- extractIds(text, prefix).forEach((id) => out.add(id));
1297
- }
1298
- }
1299
- }
1300
1596
  async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1301
1597
  const issues = [];
1302
1598
  const codeFiles = await collectFiles(srcRoot, {
@@ -1308,7 +1604,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1308
1604
  const targetFiles = [...codeFiles, ...testFiles];
1309
1605
  if (targetFiles.length === 0) {
1310
1606
  issues.push(
1311
- issue4(
1607
+ issue5(
1312
1608
  "QFAI-TRACE-001",
1313
1609
  "\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1314
1610
  "info",
@@ -1321,7 +1617,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1321
1617
  const pattern = buildIdPattern(Array.from(upstreamIds));
1322
1618
  let found = false;
1323
1619
  for (const file of targetFiles) {
1324
- const text = await readFile6(file, "utf-8");
1620
+ const text = await readFile8(file, "utf-8");
1325
1621
  if (pattern.test(text)) {
1326
1622
  found = true;
1327
1623
  break;
@@ -1329,7 +1625,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1329
1625
  }
1330
1626
  if (!found) {
1331
1627
  issues.push(
1332
- issue4(
1628
+ issue5(
1333
1629
  "QFAI-TRACE-002",
1334
1630
  "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
1335
1631
  "warning",
@@ -1344,22 +1640,22 @@ function buildIdPattern(ids) {
1344
1640
  const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1345
1641
  return new RegExp(`\\b(${escaped.join("|")})\\b`);
1346
1642
  }
1347
- function issue4(code, message, severity, file, rule, refs) {
1348
- const issue5 = {
1643
+ function issue5(code, message, severity, file, rule, refs) {
1644
+ const issue6 = {
1349
1645
  code,
1350
1646
  severity,
1351
1647
  message
1352
1648
  };
1353
1649
  if (file) {
1354
- issue5.file = file;
1650
+ issue6.file = file;
1355
1651
  }
1356
1652
  if (rule) {
1357
- issue5.rule = rule;
1653
+ issue6.rule = rule;
1358
1654
  }
1359
1655
  if (refs && refs.length > 0) {
1360
- issue5.refs = refs;
1656
+ issue6.refs = refs;
1361
1657
  }
1362
- return issue5;
1658
+ return issue6;
1363
1659
  }
1364
1660
 
1365
1661
  // src/core/validate.ts
@@ -1371,6 +1667,7 @@ async function validateProject(root, configResult) {
1371
1667
  ...await validateSpecs(root, config),
1372
1668
  ...await validateScenarios(root, config),
1373
1669
  ...await validateContracts(root, config),
1670
+ ...await validateDefinedIds(root, config),
1374
1671
  ...await validateTraceability(root, config)
1375
1672
  ];
1376
1673
  const toolVersion = await resolveToolVersion();
@@ -1383,8 +1680,8 @@ async function validateProject(root, configResult) {
1383
1680
  }
1384
1681
  function countIssues(issues) {
1385
1682
  return issues.reduce(
1386
- (acc, issue5) => {
1387
- acc[issue5.severity] += 1;
1683
+ (acc, issue6) => {
1684
+ acc[issue6.severity] += 1;
1388
1685
  return acc;
1389
1686
  },
1390
1687
  { info: 0, warning: 0, error: 0 }
@@ -1392,7 +1689,7 @@ function countIssues(issues) {
1392
1689
  }
1393
1690
 
1394
1691
  // src/core/report.ts
1395
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
1692
+ var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
1396
1693
  async function createReportData(root, validation, configResult) {
1397
1694
  const resolved = configResult ?? await loadConfig(root);
1398
1695
  const config = resolved.config;
@@ -1400,7 +1697,6 @@ async function createReportData(root, validation, configResult) {
1400
1697
  const specRoot = resolvePath(root, config, "specDir");
1401
1698
  const decisionsRoot = resolvePath(root, config, "decisionsDir");
1402
1699
  const scenariosRoot = resolvePath(root, config, "scenariosDir");
1403
- const rulesRoot = resolvePath(root, config, "rulesDir");
1404
1700
  const apiRoot = resolvePath(root, config, "apiContractsDir");
1405
1701
  const uiRoot = resolvePath(root, config, "uiContractsDir");
1406
1702
  const dbRoot = resolvePath(root, config, "dataContractsDir");
@@ -1413,7 +1709,6 @@ async function createReportData(root, validation, configResult) {
1413
1709
  const decisionFiles = await collectFiles(decisionsRoot, {
1414
1710
  extensions: [".md"]
1415
1711
  });
1416
- const ruleFiles = await collectFiles(rulesRoot, { extensions: [".md"] });
1417
1712
  const {
1418
1713
  api: apiFiles,
1419
1714
  ui: uiFiles,
@@ -1423,7 +1718,6 @@ async function createReportData(root, validation, configResult) {
1423
1718
  ...specFiles,
1424
1719
  ...scenarioFiles,
1425
1720
  ...decisionFiles,
1426
- ...ruleFiles,
1427
1721
  ...apiFiles,
1428
1722
  ...uiFiles,
1429
1723
  ...dbFiles
@@ -1449,7 +1743,6 @@ async function createReportData(root, validation, configResult) {
1449
1743
  specs: specFiles.length,
1450
1744
  scenarios: scenarioFiles.length,
1451
1745
  decisions: decisionFiles.length,
1452
- rules: ruleFiles.length,
1453
1746
  contracts: {
1454
1747
  api: apiFiles.length,
1455
1748
  ui: uiFiles.length,
@@ -1484,7 +1777,6 @@ function formatReportMarkdown(data) {
1484
1777
  lines.push(`- specs: ${data.summary.specs}`);
1485
1778
  lines.push(`- scenarios: ${data.summary.scenarios}`);
1486
1779
  lines.push(`- decisions: ${data.summary.decisions}`);
1487
- lines.push(`- rules: ${data.summary.rules}`);
1488
1780
  lines.push(
1489
1781
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
1490
1782
  );
@@ -1520,7 +1812,7 @@ function formatReportMarkdown(data) {
1520
1812
  lines.push("");
1521
1813
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
1522
1814
  const traceIssues = data.issues.filter(
1523
- (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code === "QFAI_CONTRACT_ORPHAN"
1815
+ (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-") || item.code === "QFAI_CONTRACT_ORPHAN"
1524
1816
  );
1525
1817
  if (traceIssues.length === 0) {
1526
1818
  lines.push("- (none)");
@@ -1560,8 +1852,8 @@ async function collectIds(files) {
1560
1852
  DATA: /* @__PURE__ */ new Set()
1561
1853
  };
1562
1854
  for (const file of files) {
1563
- const text = await readFile7(file, "utf-8");
1564
- for (const prefix of ID_PREFIXES) {
1855
+ const text = await readFile9(file, "utf-8");
1856
+ for (const prefix of ID_PREFIXES2) {
1565
1857
  const ids = extractIds(text, prefix);
1566
1858
  ids.forEach((id) => result[prefix].add(id));
1567
1859
  }
@@ -1578,7 +1870,7 @@ async function collectIds(files) {
1578
1870
  async function collectUpstreamIds(files) {
1579
1871
  const ids = /* @__PURE__ */ new Set();
1580
1872
  for (const file of files) {
1581
- const text = await readFile7(file, "utf-8");
1873
+ const text = await readFile9(file, "utf-8");
1582
1874
  extractAllIds(text).forEach((id) => ids.add(id));
1583
1875
  }
1584
1876
  return ids;
@@ -1599,7 +1891,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
1599
1891
  }
1600
1892
  const pattern = buildIdPattern2(Array.from(upstreamIds));
1601
1893
  for (const file of targetFiles) {
1602
- const text = await readFile7(file, "utf-8");
1894
+ const text = await readFile9(file, "utf-8");
1603
1895
  if (pattern.test(text)) {
1604
1896
  return true;
1605
1897
  }
@@ -1621,20 +1913,20 @@ function toSortedArray(values) {
1621
1913
  }
1622
1914
  function buildHotspots(issues) {
1623
1915
  const map = /* @__PURE__ */ new Map();
1624
- for (const issue5 of issues) {
1625
- if (!issue5.file) {
1916
+ for (const issue6 of issues) {
1917
+ if (!issue6.file) {
1626
1918
  continue;
1627
1919
  }
1628
- const current = map.get(issue5.file) ?? {
1629
- file: issue5.file,
1920
+ const current = map.get(issue6.file) ?? {
1921
+ file: issue6.file,
1630
1922
  total: 0,
1631
1923
  error: 0,
1632
1924
  warning: 0,
1633
1925
  info: 0
1634
1926
  };
1635
1927
  current.total += 1;
1636
- current[issue5.severity] += 1;
1637
- map.set(issue5.file, current);
1928
+ current[issue6.severity] += 1;
1929
+ map.set(issue6.file, current);
1638
1930
  }
1639
1931
  return Array.from(map.values()).sort(
1640
1932
  (a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
@@ -1643,10 +1935,10 @@ function buildHotspots(issues) {
1643
1935
 
1644
1936
  // src/cli/commands/report.ts
1645
1937
  async function runReport(options) {
1646
- const root = path9.resolve(options.root);
1938
+ const root = path10.resolve(options.root);
1647
1939
  const configResult = await loadConfig(root);
1648
1940
  const input = options.jsonPath ?? configResult.config.output.jsonPath;
1649
- const inputPath = path9.isAbsolute(input) ? input : path9.resolve(root, input);
1941
+ const inputPath = path10.isAbsolute(input) ? input : path10.resolve(root, input);
1650
1942
  let validation;
1651
1943
  try {
1652
1944
  validation = await readValidationResult(inputPath);
@@ -1671,8 +1963,8 @@ async function runReport(options) {
1671
1963
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
1672
1964
  const defaultOut = options.format === "json" ? ".qfai/out/report.json" : ".qfai/out/report.md";
1673
1965
  const out = options.outPath ?? defaultOut;
1674
- const outPath = path9.isAbsolute(out) ? out : path9.resolve(root, out);
1675
- await mkdir2(path9.dirname(outPath), { recursive: true });
1966
+ const outPath = path10.isAbsolute(out) ? out : path10.resolve(root, out);
1967
+ await mkdir2(path10.dirname(outPath), { recursive: true });
1676
1968
  await writeFile(outPath, `${output}
1677
1969
  `, "utf-8");
1678
1970
  info(
@@ -1681,7 +1973,7 @@ async function runReport(options) {
1681
1973
  info(`wrote report: ${outPath}`);
1682
1974
  }
1683
1975
  async function readValidationResult(inputPath) {
1684
- const raw = await readFile8(inputPath, "utf-8");
1976
+ const raw = await readFile10(inputPath, "utf-8");
1685
1977
  const parsed = JSON.parse(raw);
1686
1978
  if (!isValidationResult(parsed)) {
1687
1979
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -1697,17 +1989,17 @@ function isValidationResult(value) {
1697
1989
  if (!value || typeof value !== "object") {
1698
1990
  return false;
1699
1991
  }
1700
- const record = value;
1701
- if (typeof record.schemaVersion !== "string") {
1992
+ const record2 = value;
1993
+ if (typeof record2.schemaVersion !== "string") {
1702
1994
  return false;
1703
1995
  }
1704
- if (typeof record.toolVersion !== "string") {
1996
+ if (typeof record2.toolVersion !== "string") {
1705
1997
  return false;
1706
1998
  }
1707
- if (!Array.isArray(record.issues)) {
1999
+ if (!Array.isArray(record2.issues)) {
1708
2000
  return false;
1709
2001
  }
1710
- const counts = record.counts;
2002
+ const counts = record2.counts;
1711
2003
  if (!counts) {
1712
2004
  return false;
1713
2005
  }
@@ -1717,13 +2009,13 @@ function isMissingFileError(error2) {
1717
2009
  if (!error2 || typeof error2 !== "object") {
1718
2010
  return false;
1719
2011
  }
1720
- const record = error2;
1721
- return record.code === "ENOENT";
2012
+ const record2 = error2;
2013
+ return record2.code === "ENOENT";
1722
2014
  }
1723
2015
 
1724
2016
  // src/cli/commands/validate.ts
1725
2017
  import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
1726
- import path10 from "path";
2018
+ import path11 from "path";
1727
2019
 
1728
2020
  // src/cli/lib/failOn.ts
1729
2021
  function shouldFail(result, failOn) {
@@ -1738,7 +2030,7 @@ function shouldFail(result, failOn) {
1738
2030
 
1739
2031
  // src/cli/commands/validate.ts
1740
2032
  async function runValidate(options) {
1741
- const root = path10.resolve(options.root);
2033
+ const root = path11.resolve(options.root);
1742
2034
  const configResult = await loadConfig(root);
1743
2035
  const result = await validateProject(root, configResult);
1744
2036
  const format = options.format ?? configResult.config.output.format;
@@ -1782,20 +2074,20 @@ function emitText(result) {
1782
2074
  `
1783
2075
  );
1784
2076
  }
1785
- function emitGitHub(issue5) {
1786
- const level = issue5.severity === "error" ? "error" : issue5.severity === "warning" ? "warning" : "notice";
1787
- const file = issue5.file ? `file=${issue5.file}` : "";
1788
- const line = issue5.loc?.line ? `,line=${issue5.loc.line}` : "";
1789
- const column = issue5.loc?.column ? `,col=${issue5.loc.column}` : "";
2077
+ function emitGitHub(issue6) {
2078
+ const level = issue6.severity === "error" ? "error" : issue6.severity === "warning" ? "warning" : "notice";
2079
+ const file = issue6.file ? `file=${issue6.file}` : "";
2080
+ const line = issue6.loc?.line ? `,line=${issue6.loc.line}` : "";
2081
+ const column = issue6.loc?.column ? `,col=${issue6.loc.column}` : "";
1790
2082
  const location = file ? ` ${file}${line}${column}` : "";
1791
2083
  process.stdout.write(
1792
- `::${level}${location}::${issue5.code}: ${issue5.message}
2084
+ `::${level}${location}::${issue6.code}: ${issue6.message}
1793
2085
  `
1794
2086
  );
1795
2087
  }
1796
2088
  async function emitJson(result, root, jsonPath) {
1797
- const abs = path10.isAbsolute(jsonPath) ? jsonPath : path10.resolve(root, jsonPath);
1798
- await mkdir3(path10.dirname(abs), { recursive: true });
2089
+ const abs = path11.isAbsolute(jsonPath) ? jsonPath : path11.resolve(root, jsonPath);
2090
+ await mkdir3(path11.dirname(abs), { recursive: true });
1799
2091
  await writeFile2(abs, `${JSON.stringify(result, null, 2)}
1800
2092
  `, "utf-8");
1801
2093
  }