qfai 0.4.9 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(src_exports, {
35
35
  extractAllIds: () => extractAllIds,
36
36
  extractIds: () => extractIds,
37
37
  extractInvalidIds: () => extractInvalidIds,
38
+ findConfigRoot: () => findConfigRoot,
38
39
  formatReportJson: () => formatReportJson,
39
40
  formatReportMarkdown: () => formatReportMarkdown,
40
41
  getConfigPath: () => getConfigPath,
@@ -87,7 +88,7 @@ var defaultConfig = {
87
88
  testFileGlobs: [],
88
89
  testFileExcludeGlobs: [],
89
90
  scNoTestSeverity: "error",
90
- allowOrphanContracts: false,
91
+ orphanContractsPolicy: "error",
91
92
  unknownContractIdSeverity: "error"
92
93
  }
93
94
  },
@@ -98,6 +99,26 @@ var defaultConfig = {
98
99
  function getConfigPath(root) {
99
100
  return import_node_path.default.join(root, "qfai.config.yaml");
100
101
  }
102
+ async function findConfigRoot(startDir) {
103
+ const resolvedStart = import_node_path.default.resolve(startDir);
104
+ let current = resolvedStart;
105
+ while (true) {
106
+ const configPath = getConfigPath(current);
107
+ if (await exists(configPath)) {
108
+ return { root: current, configPath, found: true };
109
+ }
110
+ const parent = import_node_path.default.dirname(current);
111
+ if (parent === current) {
112
+ break;
113
+ }
114
+ current = parent;
115
+ }
116
+ return {
117
+ root: resolvedStart,
118
+ configPath: getConfigPath(resolvedStart),
119
+ found: false
120
+ };
121
+ }
101
122
  async function loadConfig(root) {
102
123
  const configPath = getConfigPath(root);
103
124
  const issues = [];
@@ -287,10 +308,10 @@ function normalizeValidation(raw, configPath, issues) {
287
308
  configPath,
288
309
  issues
289
310
  ),
290
- allowOrphanContracts: readBoolean(
291
- traceabilityRaw?.allowOrphanContracts,
292
- base.traceability.allowOrphanContracts,
293
- "validation.traceability.allowOrphanContracts",
311
+ orphanContractsPolicy: readOrphanContractsPolicy(
312
+ traceabilityRaw?.orphanContractsPolicy,
313
+ base.traceability.orphanContractsPolicy,
314
+ "validation.traceability.orphanContractsPolicy",
294
315
  configPath,
295
316
  issues
296
317
  ),
@@ -386,6 +407,20 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
386
407
  }
387
408
  return fallback;
388
409
  }
410
+ function readOrphanContractsPolicy(value, fallback, label, configPath, issues) {
411
+ if (value === "error" || value === "warning" || value === "allow") {
412
+ return value;
413
+ }
414
+ if (value !== void 0) {
415
+ issues.push(
416
+ configIssue(
417
+ configPath,
418
+ `${label} \u306F error|warning|allow \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
419
+ )
420
+ );
421
+ }
422
+ return fallback;
423
+ }
389
424
  function configIssue(file, message) {
390
425
  return {
391
426
  code: "QFAI_CONFIG_INVALID",
@@ -401,6 +436,14 @@ function isMissingFile(error) {
401
436
  }
402
437
  return false;
403
438
  }
439
+ async function exists(target) {
440
+ try {
441
+ await (0, import_promises.access)(target);
442
+ return true;
443
+ } catch {
444
+ return false;
445
+ }
446
+ }
404
447
  function formatError(error) {
405
448
  if (error instanceof Error) {
406
449
  return error.message;
@@ -466,7 +509,7 @@ function isValidId(value, prefix) {
466
509
 
467
510
  // src/core/report.ts
468
511
  var import_promises14 = require("fs/promises");
469
- var import_node_path11 = __toESM(require("path"), 1);
512
+ var import_node_path12 = __toESM(require("path"), 1);
470
513
 
471
514
  // src/core/contractIndex.ts
472
515
  var import_promises5 = require("fs/promises");
@@ -489,7 +532,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
489
532
  ]);
490
533
  async function collectFiles(root, options = {}) {
491
534
  const entries = [];
492
- if (!await exists(root)) {
535
+ if (!await exists2(root)) {
493
536
  return entries;
494
537
  }
495
538
  const ignoreDirs = /* @__PURE__ */ new Set([
@@ -534,7 +577,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
534
577
  }
535
578
  }
536
579
  }
537
- async function exists(target) {
580
+ async function exists2(target) {
538
581
  try {
539
582
  await (0, import_promises2.access)(target);
540
583
  return true;
@@ -608,13 +651,13 @@ async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
608
651
  async function filterExisting(files) {
609
652
  const existing = [];
610
653
  for (const file of files) {
611
- if (await exists2(file)) {
654
+ if (await exists3(file)) {
612
655
  existing.push(file);
613
656
  }
614
657
  }
615
658
  return existing;
616
659
  }
617
- async function exists2(target) {
660
+ async function exists3(target) {
618
661
  try {
619
662
  await (0, import_promises4.access)(target);
620
663
  return true;
@@ -674,6 +717,113 @@ function record(index, id, file) {
674
717
  index.idToFiles.set(id, current);
675
718
  }
676
719
 
720
+ // src/core/paths.ts
721
+ var import_node_path5 = __toESM(require("path"), 1);
722
+ function toRelativePath(root, target) {
723
+ if (!target) {
724
+ return target;
725
+ }
726
+ if (!import_node_path5.default.isAbsolute(target)) {
727
+ return toPosixPath(target);
728
+ }
729
+ const relative = import_node_path5.default.relative(root, target);
730
+ if (!relative) {
731
+ return ".";
732
+ }
733
+ return toPosixPath(relative);
734
+ }
735
+ function toPosixPath(value) {
736
+ return value.replace(/\\/g, "/");
737
+ }
738
+
739
+ // src/core/normalize.ts
740
+ function normalizeIssuePaths(root, issues) {
741
+ return issues.map((issue7) => {
742
+ if (!issue7.file) {
743
+ return issue7;
744
+ }
745
+ const normalized = toRelativePath(root, issue7.file);
746
+ if (normalized === issue7.file) {
747
+ return issue7;
748
+ }
749
+ return {
750
+ ...issue7,
751
+ file: normalized
752
+ };
753
+ });
754
+ }
755
+ function normalizeScCoverage(root, sc) {
756
+ const refs = {};
757
+ for (const [scId, files] of Object.entries(sc.refs)) {
758
+ refs[scId] = files.map((file) => toRelativePath(root, file));
759
+ }
760
+ return {
761
+ ...sc,
762
+ refs
763
+ };
764
+ }
765
+ function normalizeValidationResult(root, result) {
766
+ return {
767
+ ...result,
768
+ issues: normalizeIssuePaths(root, result.issues),
769
+ traceability: {
770
+ ...result.traceability,
771
+ sc: normalizeScCoverage(root, result.traceability.sc)
772
+ }
773
+ };
774
+ }
775
+
776
+ // src/core/parse/contractRefs.ts
777
+ var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
778
+ function parseContractRefs(text, options = {}) {
779
+ const linePattern = buildLinePattern(options);
780
+ const lines = [];
781
+ for (const match of text.matchAll(linePattern)) {
782
+ lines.push((match[1] ?? "").trim());
783
+ }
784
+ const ids = [];
785
+ const invalidTokens = [];
786
+ let hasNone = false;
787
+ for (const line of lines) {
788
+ if (line.length === 0) {
789
+ invalidTokens.push("(empty)");
790
+ continue;
791
+ }
792
+ const tokens = line.split(",").map((token) => token.trim());
793
+ for (const token of tokens) {
794
+ if (token.length === 0) {
795
+ invalidTokens.push("(empty)");
796
+ continue;
797
+ }
798
+ if (token === "none") {
799
+ hasNone = true;
800
+ continue;
801
+ }
802
+ if (CONTRACT_REF_ID_RE.test(token)) {
803
+ ids.push(token);
804
+ continue;
805
+ }
806
+ invalidTokens.push(token);
807
+ }
808
+ }
809
+ return {
810
+ lines,
811
+ ids: unique2(ids),
812
+ invalidTokens: unique2(invalidTokens),
813
+ hasNone
814
+ };
815
+ }
816
+ function buildLinePattern(options) {
817
+ const prefix = options.allowCommentPrefix ? "#" : "";
818
+ return new RegExp(
819
+ `^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
820
+ "gm"
821
+ );
822
+ }
823
+ function unique2(values) {
824
+ return Array.from(new Set(values));
825
+ }
826
+
677
827
  // src/core/parse/markdown.ts
678
828
  var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
679
829
  function parseHeadings(md) {
@@ -720,8 +870,6 @@ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
720
870
  var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
721
871
  var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
722
872
  var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
723
- var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
724
- var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
725
873
  var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
726
874
  var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
727
875
  function parseSpec(md, file) {
@@ -794,50 +942,10 @@ function parseSpec(md, file) {
794
942
  }
795
943
  return parsed;
796
944
  }
797
- function parseContractRefs(md) {
798
- const lines = [];
799
- for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
800
- lines.push((match[1] ?? "").trim());
801
- }
802
- const ids = [];
803
- const invalidTokens = [];
804
- let hasNone = false;
805
- for (const line of lines) {
806
- if (line.length === 0) {
807
- invalidTokens.push("(empty)");
808
- continue;
809
- }
810
- const tokens = line.split(",").map((token) => token.trim());
811
- for (const token of tokens) {
812
- if (token.length === 0) {
813
- invalidTokens.push("(empty)");
814
- continue;
815
- }
816
- if (token === "none") {
817
- hasNone = true;
818
- continue;
819
- }
820
- if (CONTRACT_REF_ID_RE.test(token)) {
821
- ids.push(token);
822
- continue;
823
- }
824
- invalidTokens.push(token);
825
- }
826
- }
827
- return {
828
- lines,
829
- ids: unique2(ids),
830
- invalidTokens: unique2(invalidTokens),
831
- hasNone
832
- };
833
- }
834
- function unique2(values) {
835
- return Array.from(new Set(values));
836
- }
837
945
 
838
946
  // src/core/traceability.ts
839
947
  var import_promises6 = require("fs/promises");
840
- var import_node_path5 = __toESM(require("path"), 1);
948
+ var import_node_path6 = __toESM(require("path"), 1);
841
949
 
842
950
  // src/core/gherkin/parse.ts
843
951
  var import_gherkin = require("@cucumber/gherkin");
@@ -868,9 +976,6 @@ function formatError2(error) {
868
976
  var SPEC_TAG_RE = /^SPEC-\d{4}$/;
869
977
  var SC_TAG_RE = /^SC-\d{4}$/;
870
978
  var BR_TAG_RE = /^BR-\d{4}$/;
871
- var UI_TAG_RE = /^UI-\d{4}$/;
872
- var API_TAG_RE = /^API-\d{4}$/;
873
- var DB_TAG_RE = /^DB-\d{4}$/;
874
979
  function parseScenarioDocument(text, uri) {
875
980
  const { gherkinDocument, errors } = parseGherkin(text, uri);
876
981
  if (!gherkinDocument) {
@@ -895,31 +1000,21 @@ function parseScenarioDocument(text, uri) {
895
1000
  errors
896
1001
  };
897
1002
  }
898
- function buildScenarioAtoms(document) {
1003
+ function buildScenarioAtoms(document, contractIds = []) {
1004
+ const uniqueContractIds = unique3(contractIds).sort(
1005
+ (a, b) => a.localeCompare(b)
1006
+ );
899
1007
  return document.scenarios.map((scenario) => {
900
1008
  const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
901
1009
  const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
902
1010
  const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
903
- const contractIds = /* @__PURE__ */ new Set();
904
- scenario.tags.forEach((tag) => {
905
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
906
- contractIds.add(tag);
907
- }
908
- });
909
- for (const step of scenario.steps) {
910
- for (const text of collectStepTexts(step)) {
911
- extractIds(text, "UI").forEach((id) => contractIds.add(id));
912
- extractIds(text, "API").forEach((id) => contractIds.add(id));
913
- extractIds(text, "DB").forEach((id) => contractIds.add(id));
914
- }
915
- }
916
1011
  const atom = {
917
1012
  uri: document.uri,
918
1013
  featureName: document.featureName ?? "",
919
1014
  scenarioName: scenario.name,
920
1015
  kind: scenario.kind,
921
1016
  brIds,
922
- contractIds: Array.from(contractIds).sort()
1017
+ contractIds: uniqueContractIds
923
1018
  };
924
1019
  if (scenario.line !== void 0) {
925
1020
  atom.line = scenario.line;
@@ -972,23 +1067,6 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
972
1067
  function collectTagNames(tags) {
973
1068
  return tags.map((tag) => tag.name.replace(/^@/, ""));
974
1069
  }
975
- function collectStepTexts(step) {
976
- const texts = [];
977
- if (step.text) {
978
- texts.push(step.text);
979
- }
980
- if (step.docString?.content) {
981
- texts.push(step.docString.content);
982
- }
983
- if (step.dataTable?.rows) {
984
- for (const row of step.dataTable.rows) {
985
- for (const cell of row.cells) {
986
- texts.push(cell.value);
987
- }
988
- }
989
- }
990
- return texts;
991
- }
992
1070
  function unique3(values) {
993
1071
  return Array.from(new Set(values));
994
1072
  }
@@ -1090,7 +1168,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1090
1168
  };
1091
1169
  }
1092
1170
  const normalizedFiles = Array.from(
1093
- new Set(files.map((file) => import_node_path5.default.normalize(file)))
1171
+ new Set(files.map((file) => import_node_path6.default.normalize(file)))
1094
1172
  );
1095
1173
  for (const file of normalizedFiles) {
1096
1174
  const text = await (0, import_promises6.readFile)(file, "utf-8");
@@ -1151,11 +1229,11 @@ function formatError3(error) {
1151
1229
 
1152
1230
  // src/core/version.ts
1153
1231
  var import_promises7 = require("fs/promises");
1154
- var import_node_path6 = __toESM(require("path"), 1);
1232
+ var import_node_path7 = __toESM(require("path"), 1);
1155
1233
  var import_node_url = require("url");
1156
1234
  async function resolveToolVersion() {
1157
- if ("0.4.9".length > 0) {
1158
- return "0.4.9";
1235
+ if ("0.5.2".length > 0) {
1236
+ return "0.5.2";
1159
1237
  }
1160
1238
  try {
1161
1239
  const packagePath = resolvePackageJsonPath();
@@ -1170,18 +1248,18 @@ async function resolveToolVersion() {
1170
1248
  function resolvePackageJsonPath() {
1171
1249
  const base = __filename;
1172
1250
  const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
1173
- return import_node_path6.default.resolve(import_node_path6.default.dirname(basePath), "../../package.json");
1251
+ return import_node_path7.default.resolve(import_node_path7.default.dirname(basePath), "../../package.json");
1174
1252
  }
1175
1253
 
1176
1254
  // src/core/validators/contracts.ts
1177
1255
  var import_promises8 = require("fs/promises");
1178
- var import_node_path8 = __toESM(require("path"), 1);
1256
+ var import_node_path9 = __toESM(require("path"), 1);
1179
1257
 
1180
1258
  // src/core/contracts.ts
1181
- var import_node_path7 = __toESM(require("path"), 1);
1259
+ var import_node_path8 = __toESM(require("path"), 1);
1182
1260
  var import_yaml2 = require("yaml");
1183
1261
  function parseStructuredContract(file, text) {
1184
- const ext = import_node_path7.default.extname(file).toLowerCase();
1262
+ const ext = import_node_path8.default.extname(file).toLowerCase();
1185
1263
  if (ext === ".json") {
1186
1264
  return JSON.parse(text);
1187
1265
  }
@@ -1201,9 +1279,9 @@ var SQL_DANGEROUS_PATTERNS = [
1201
1279
  async function validateContracts(root, config) {
1202
1280
  const issues = [];
1203
1281
  const contractsRoot = resolvePath(root, config, "contractsDir");
1204
- issues.push(...await validateUiContracts(import_node_path8.default.join(contractsRoot, "ui")));
1205
- issues.push(...await validateApiContracts(import_node_path8.default.join(contractsRoot, "api")));
1206
- issues.push(...await validateDbContracts(import_node_path8.default.join(contractsRoot, "db")));
1282
+ issues.push(...await validateUiContracts(import_node_path9.default.join(contractsRoot, "ui")));
1283
+ issues.push(...await validateApiContracts(import_node_path9.default.join(contractsRoot, "api")));
1284
+ issues.push(...await validateDbContracts(import_node_path9.default.join(contractsRoot, "db")));
1207
1285
  const contractIndex = await buildContractIndex(root, config);
1208
1286
  issues.push(...validateDuplicateContractIds(contractIndex));
1209
1287
  return issues;
@@ -1486,7 +1564,7 @@ function issue(code, message, severity, file, rule, refs) {
1486
1564
 
1487
1565
  // src/core/validators/delta.ts
1488
1566
  var import_promises9 = require("fs/promises");
1489
- var import_node_path9 = __toESM(require("path"), 1);
1567
+ var import_node_path10 = __toESM(require("path"), 1);
1490
1568
  var SECTION_RE = /^##\s+変更区分/m;
1491
1569
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1492
1570
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1500,7 +1578,7 @@ async function validateDeltas(root, config) {
1500
1578
  }
1501
1579
  const issues = [];
1502
1580
  for (const pack of packs) {
1503
- const deltaPath = import_node_path9.default.join(pack, "delta.md");
1581
+ const deltaPath = import_node_path10.default.join(pack, "delta.md");
1504
1582
  let text;
1505
1583
  try {
1506
1584
  text = await (0, import_promises9.readFile)(deltaPath, "utf-8");
@@ -1576,7 +1654,7 @@ function issue2(code, message, severity, file, rule, refs) {
1576
1654
 
1577
1655
  // src/core/validators/ids.ts
1578
1656
  var import_promises10 = require("fs/promises");
1579
- var import_node_path10 = __toESM(require("path"), 1);
1657
+ var import_node_path11 = __toESM(require("path"), 1);
1580
1658
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1581
1659
  async function validateDefinedIds(root, config) {
1582
1660
  const issues = [];
@@ -1642,7 +1720,7 @@ function recordId(out, id, file) {
1642
1720
  }
1643
1721
  function formatFileList(files, root) {
1644
1722
  return files.map((file) => {
1645
- const relative = import_node_path10.default.relative(root, file);
1723
+ const relative = import_node_path11.default.relative(root, file);
1646
1724
  return relative.length > 0 ? relative : file;
1647
1725
  }).join(", ");
1648
1726
  }
@@ -2079,7 +2157,7 @@ async function validateTraceability(root, config) {
2079
2157
  if (contractRefs.hasNone && contractRefs.ids.length > 0) {
2080
2158
  issues.push(
2081
2159
  issue6(
2082
- "QFAI-TRACE-021",
2160
+ "QFAI-TRACE-023",
2083
2161
  "Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2084
2162
  "error",
2085
2163
  file,
@@ -2111,7 +2189,7 @@ async function validateTraceability(root, config) {
2111
2189
  if (unknownContractIds.length > 0) {
2112
2190
  issues.push(
2113
2191
  issue6(
2114
- "QFAI-TRACE-021",
2192
+ "QFAI-TRACE-024",
2115
2193
  `Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2116
2194
  ", "
2117
2195
  )}`,
@@ -2126,11 +2204,62 @@ async function validateTraceability(root, config) {
2126
2204
  for (const file of scenarioFiles) {
2127
2205
  const text = await (0, import_promises13.readFile)(file, "utf-8");
2128
2206
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2207
+ const scenarioContractRefs = parseContractRefs(text, {
2208
+ allowCommentPrefix: true
2209
+ });
2210
+ if (scenarioContractRefs.lines.length === 0) {
2211
+ issues.push(
2212
+ issue6(
2213
+ "QFAI-TRACE-031",
2214
+ "Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2215
+ "error",
2216
+ file,
2217
+ "traceability.scenarioContractRefRequired"
2218
+ )
2219
+ );
2220
+ } else {
2221
+ if (scenarioContractRefs.hasNone && scenarioContractRefs.ids.length > 0) {
2222
+ issues.push(
2223
+ issue6(
2224
+ "QFAI-TRACE-033",
2225
+ "Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2226
+ "error",
2227
+ file,
2228
+ "traceability.scenarioContractRefFormat"
2229
+ )
2230
+ );
2231
+ }
2232
+ if (scenarioContractRefs.invalidTokens.length > 0) {
2233
+ issues.push(
2234
+ issue6(
2235
+ "QFAI-TRACE-032",
2236
+ `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
2237
+ ", "
2238
+ )}`,
2239
+ "error",
2240
+ file,
2241
+ "traceability.scenarioContractRefFormat",
2242
+ scenarioContractRefs.invalidTokens
2243
+ )
2244
+ );
2245
+ }
2246
+ }
2129
2247
  const { document, errors } = parseScenarioDocument(text, file);
2130
2248
  if (!document || errors.length > 0) {
2131
2249
  continue;
2132
2250
  }
2133
- const atoms = buildScenarioAtoms(document);
2251
+ if (document.scenarios.length !== 1) {
2252
+ issues.push(
2253
+ issue6(
2254
+ "QFAI-TRACE-030",
2255
+ `Scenario \u30D5\u30A1\u30A4\u30EB\u306F 1\u30D5\u30A1\u30A4\u30EB=1\u30B7\u30CA\u30EA\u30AA\u3067\u3059\u3002\u73FE\u5728: ${document.scenarios.length}\u4EF6 (file=${file})`,
2256
+ "error",
2257
+ file,
2258
+ "traceability.scenarioOnePerFile"
2259
+ )
2260
+ );
2261
+ }
2262
+ const atoms = buildScenarioAtoms(document, scenarioContractRefs.ids);
2134
2263
  const scIdsInFile = /* @__PURE__ */ new Set();
2135
2264
  for (const [index, scenario] of document.scenarios.entries()) {
2136
2265
  const atom = atoms[index];
@@ -2275,7 +2404,7 @@ async function validateTraceability(root, config) {
2275
2404
  if (orphanBrIds.length > 0) {
2276
2405
  issues.push(
2277
2406
  issue6(
2278
- "QFAI_TRACE_BR_ORPHAN",
2407
+ "QFAI-TRACE-009",
2279
2408
  `BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
2280
2409
  "error",
2281
2410
  specsRoot,
@@ -2345,17 +2474,19 @@ async function validateTraceability(root, config) {
2345
2474
  );
2346
2475
  }
2347
2476
  }
2348
- if (!config.validation.traceability.allowOrphanContracts) {
2477
+ const orphanPolicy = config.validation.traceability.orphanContractsPolicy;
2478
+ if (orphanPolicy !== "allow") {
2349
2479
  if (contractIds.size > 0) {
2350
2480
  const orphanContracts = Array.from(contractIds).filter(
2351
2481
  (id) => !specContractIds.has(id)
2352
2482
  );
2353
2483
  if (orphanContracts.length > 0) {
2484
+ const severity = orphanPolicy === "warning" ? "warning" : "error";
2354
2485
  issues.push(
2355
2486
  issue6(
2356
2487
  "QFAI-TRACE-022",
2357
2488
  `\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2358
- "error",
2489
+ severity,
2359
2490
  specsRoot,
2360
2491
  "traceability.contractCoverage",
2361
2492
  orphanContracts
@@ -2480,16 +2611,17 @@ function countIssues(issues) {
2480
2611
  // src/core/report.ts
2481
2612
  var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2482
2613
  async function createReportData(root, validation, configResult) {
2483
- const resolved = configResult ?? await loadConfig(root);
2614
+ const resolvedRoot = import_node_path12.default.resolve(root);
2615
+ const resolved = configResult ?? await loadConfig(resolvedRoot);
2484
2616
  const config = resolved.config;
2485
2617
  const configPath = resolved.configPath;
2486
- const specsRoot = resolvePath(root, config, "specsDir");
2487
- const contractsRoot = resolvePath(root, config, "contractsDir");
2488
- const apiRoot = import_node_path11.default.join(contractsRoot, "api");
2489
- const uiRoot = import_node_path11.default.join(contractsRoot, "ui");
2490
- const dbRoot = import_node_path11.default.join(contractsRoot, "db");
2491
- const srcRoot = resolvePath(root, config, "srcDir");
2492
- const testsRoot = resolvePath(root, config, "testsDir");
2618
+ const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
2619
+ const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
2620
+ const apiRoot = import_node_path12.default.join(contractsRoot, "api");
2621
+ const uiRoot = import_node_path12.default.join(contractsRoot, "ui");
2622
+ const dbRoot = import_node_path12.default.join(contractsRoot, "db");
2623
+ const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
2624
+ const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
2493
2625
  const specFiles = await collectSpecFiles(specsRoot);
2494
2626
  const scenarioFiles = await collectScenarioFiles(specsRoot);
2495
2627
  const {
@@ -2497,12 +2629,15 @@ async function createReportData(root, validation, configResult) {
2497
2629
  ui: uiFiles,
2498
2630
  db: dbFiles
2499
2631
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
2500
- const contractIndex = await buildContractIndex(root, config);
2501
- const specContractRefs = await collectSpecContractRefs(specFiles);
2632
+ const contractIndex = await buildContractIndex(resolvedRoot, config);
2502
2633
  const contractIdList = Array.from(contractIndex.ids);
2634
+ const specContractRefs = await collectSpecContractRefs(
2635
+ specFiles,
2636
+ contractIdList
2637
+ );
2503
2638
  const referencedContracts = /* @__PURE__ */ new Set();
2504
- for (const ids of specContractRefs.specToContractIds.values()) {
2505
- ids.forEach((id) => referencedContracts.add(id));
2639
+ for (const entry of specContractRefs.specToContracts.values()) {
2640
+ entry.ids.forEach((id) => referencedContracts.add(id));
2506
2641
  }
2507
2642
  const referencedContractCount = contractIdList.filter(
2508
2643
  (id) => referencedContracts.has(id)
@@ -2511,8 +2646,8 @@ async function createReportData(root, validation, configResult) {
2511
2646
  (id) => !referencedContracts.has(id)
2512
2647
  ).length;
2513
2648
  const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
2514
- const specToContractIdsRecord = mapToSortedRecord(
2515
- specContractRefs.specToContractIds
2649
+ const specToContractsRecord = mapToSpecContractRecord(
2650
+ specContractRefs.specToContracts
2516
2651
  );
2517
2652
  const idsByPrefix = await collectIds([
2518
2653
  ...specFiles,
@@ -2530,24 +2665,26 @@ async function createReportData(root, validation, configResult) {
2530
2665
  srcRoot,
2531
2666
  testsRoot
2532
2667
  );
2533
- const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2534
- const scRefsResult = await collectScTestReferences(
2535
- root,
2536
- config.validation.traceability.testFileGlobs,
2537
- config.validation.traceability.testFileExcludeGlobs
2668
+ const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
2669
+ const normalizedValidation = normalizeValidationResult(
2670
+ resolvedRoot,
2671
+ resolvedValidationRaw
2538
2672
  );
2539
- const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2540
- const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2673
+ const scCoverage = normalizedValidation.traceability.sc;
2674
+ const testFiles = normalizedValidation.traceability.testFiles;
2541
2675
  const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2542
- const scSourceRecord = mapToSortedRecord(scSources);
2543
- const resolvedValidation = validation ?? await validateProject(root, resolved);
2676
+ const scSourceRecord = mapToSortedRecord(
2677
+ normalizeScSources(resolvedRoot, scSources)
2678
+ );
2544
2679
  const version = await resolveToolVersion();
2680
+ const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
2681
+ const displayConfigPath = toRelativePath(resolvedRoot, configPath);
2545
2682
  return {
2546
2683
  tool: "qfai",
2547
2684
  version,
2548
2685
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2549
- root,
2550
- configPath,
2686
+ root: displayRoot,
2687
+ configPath: displayConfigPath,
2551
2688
  summary: {
2552
2689
  specs: specFiles.length,
2553
2690
  scenarios: scenarioFiles.length,
@@ -2556,7 +2693,7 @@ async function createReportData(root, validation, configResult) {
2556
2693
  ui: uiFiles.length,
2557
2694
  db: dbFiles.length
2558
2695
  },
2559
- counts: resolvedValidation.counts
2696
+ counts: normalizedValidation.counts
2560
2697
  },
2561
2698
  ids: {
2562
2699
  spec: idsByPrefix.SPEC,
@@ -2580,21 +2717,24 @@ async function createReportData(root, validation, configResult) {
2580
2717
  },
2581
2718
  specs: {
2582
2719
  contractRefMissing: specContractRefs.missingRefSpecs.size,
2583
- specToContractIds: specToContractIdsRecord
2720
+ missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
2721
+ specToContracts: specToContractsRecord
2584
2722
  }
2585
2723
  },
2586
- issues: resolvedValidation.issues
2724
+ issues: normalizedValidation.issues
2587
2725
  };
2588
2726
  }
2589
2727
  function formatReportMarkdown(data) {
2590
2728
  const lines = [];
2591
2729
  lines.push("# QFAI Report");
2730
+ lines.push("");
2592
2731
  lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
2593
2732
  lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
2594
2733
  lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
2595
2734
  lines.push(`- \u7248: ${data.version}`);
2596
2735
  lines.push("");
2597
2736
  lines.push("## \u6982\u8981");
2737
+ lines.push("");
2598
2738
  lines.push(`- specs: ${data.summary.specs}`);
2599
2739
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2600
2740
  lines.push(
@@ -2605,6 +2745,7 @@ function formatReportMarkdown(data) {
2605
2745
  );
2606
2746
  lines.push("");
2607
2747
  lines.push("## ID\u96C6\u8A08");
2748
+ lines.push("");
2608
2749
  lines.push(formatIdLine("SPEC", data.ids.spec));
2609
2750
  lines.push(formatIdLine("BR", data.ids.br));
2610
2751
  lines.push(formatIdLine("SC", data.ids.sc));
@@ -2613,12 +2754,14 @@ function formatReportMarkdown(data) {
2613
2754
  lines.push(formatIdLine("DB", data.ids.db));
2614
2755
  lines.push("");
2615
2756
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
2757
+ lines.push("");
2616
2758
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
2617
2759
  lines.push(
2618
2760
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2619
2761
  );
2620
2762
  lines.push("");
2621
2763
  lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
2764
+ lines.push("");
2622
2765
  lines.push(`- total: ${data.traceability.contracts.total}`);
2623
2766
  lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
2624
2767
  lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
@@ -2627,6 +2770,7 @@ function formatReportMarkdown(data) {
2627
2770
  );
2628
2771
  lines.push("");
2629
2772
  lines.push("## \u5951\u7D04\u2192Spec");
2773
+ lines.push("");
2630
2774
  const contractToSpecs = data.traceability.contracts.idToSpecs;
2631
2775
  const contractIds = Object.keys(contractToSpecs).sort(
2632
2776
  (a, b) => a.localeCompare(b)
@@ -2645,24 +2789,36 @@ function formatReportMarkdown(data) {
2645
2789
  }
2646
2790
  lines.push("");
2647
2791
  lines.push("## Spec\u2192\u5951\u7D04");
2648
- const specToContracts = data.traceability.specs.specToContractIds;
2792
+ lines.push("");
2793
+ const specToContracts = data.traceability.specs.specToContracts;
2649
2794
  const specIds = Object.keys(specToContracts).sort(
2650
2795
  (a, b) => a.localeCompare(b)
2651
2796
  );
2652
2797
  if (specIds.length === 0) {
2653
2798
  lines.push("- (none)");
2654
2799
  } else {
2655
- for (const specId of specIds) {
2656
- const contractIds2 = specToContracts[specId] ?? [];
2657
- if (contractIds2.length === 0) {
2658
- lines.push(`- ${specId}: (none)`);
2659
- } else {
2660
- lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
2661
- }
2800
+ const rows = specIds.map((specId) => {
2801
+ const entry = specToContracts[specId];
2802
+ const contracts = entry?.status === "missing" ? "(missing)" : entry && entry.ids.length > 0 ? entry.ids.join(", ") : "(none)";
2803
+ const status = entry?.status ?? "missing";
2804
+ return [specId, status, contracts];
2805
+ });
2806
+ lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
2807
+ }
2808
+ lines.push("");
2809
+ lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
2810
+ lines.push("");
2811
+ const missingRefSpecs = data.traceability.specs.missingRefSpecs;
2812
+ if (missingRefSpecs.length === 0) {
2813
+ lines.push("- (none)");
2814
+ } else {
2815
+ for (const specId of missingRefSpecs) {
2816
+ lines.push(`- ${specId}`);
2662
2817
  }
2663
2818
  }
2664
2819
  lines.push("");
2665
2820
  lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2821
+ lines.push("");
2666
2822
  lines.push(`- total: ${data.traceability.sc.total}`);
2667
2823
  lines.push(`- covered: ${data.traceability.sc.covered}`);
2668
2824
  lines.push(`- missing: ${data.traceability.sc.missing}`);
@@ -2692,6 +2848,7 @@ function formatReportMarkdown(data) {
2692
2848
  }
2693
2849
  lines.push("");
2694
2850
  lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
2851
+ lines.push("");
2695
2852
  const scRefs = data.traceability.sc.refs;
2696
2853
  const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2697
2854
  if (scIds.length === 0) {
@@ -2708,6 +2865,7 @@ function formatReportMarkdown(data) {
2708
2865
  }
2709
2866
  lines.push("");
2710
2867
  lines.push("## Spec:SC=1:1 \u9055\u53CD");
2868
+ lines.push("");
2711
2869
  const specScIssues = data.issues.filter(
2712
2870
  (item) => item.code === "QFAI-TRACE-012"
2713
2871
  );
@@ -2722,6 +2880,7 @@ function formatReportMarkdown(data) {
2722
2880
  }
2723
2881
  lines.push("");
2724
2882
  lines.push("## Hotspots");
2883
+ lines.push("");
2725
2884
  const hotspots = buildHotspots(data.issues);
2726
2885
  if (hotspots.length === 0) {
2727
2886
  lines.push("- (none)");
@@ -2734,6 +2893,7 @@ function formatReportMarkdown(data) {
2734
2893
  }
2735
2894
  lines.push("");
2736
2895
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
2896
+ lines.push("");
2737
2897
  const traceIssues = data.issues.filter(
2738
2898
  (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
2739
2899
  );
@@ -2749,6 +2909,7 @@ function formatReportMarkdown(data) {
2749
2909
  }
2750
2910
  lines.push("");
2751
2911
  lines.push("## \u691C\u8A3C\u7D50\u679C");
2912
+ lines.push("");
2752
2913
  if (data.issues.length === 0) {
2753
2914
  lines.push("- (none)");
2754
2915
  } else {
@@ -2765,29 +2926,41 @@ function formatReportMarkdown(data) {
2765
2926
  function formatReportJson(data) {
2766
2927
  return JSON.stringify(data, null, 2);
2767
2928
  }
2768
- async function collectSpecContractRefs(specFiles) {
2769
- const specToContractIds = /* @__PURE__ */ new Map();
2929
+ async function collectSpecContractRefs(specFiles, contractIdList) {
2930
+ const specToContracts = /* @__PURE__ */ new Map();
2770
2931
  const idToSpecs = /* @__PURE__ */ new Map();
2771
2932
  const missingRefSpecs = /* @__PURE__ */ new Set();
2933
+ for (const contractId of contractIdList) {
2934
+ idToSpecs.set(contractId, /* @__PURE__ */ new Set());
2935
+ }
2772
2936
  for (const file of specFiles) {
2773
2937
  const text = await (0, import_promises14.readFile)(file, "utf-8");
2774
2938
  const parsed = parseSpec(text, file);
2775
- const specKey = parsed.specId ?? file;
2939
+ const specKey = parsed.specId;
2940
+ if (!specKey) {
2941
+ continue;
2942
+ }
2776
2943
  const refs = parsed.contractRefs;
2777
2944
  if (refs.lines.length === 0) {
2778
2945
  missingRefSpecs.add(specKey);
2946
+ specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
2947
+ continue;
2779
2948
  }
2780
- const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
2949
+ const current = specToContracts.get(specKey) ?? {
2950
+ status: "declared",
2951
+ ids: /* @__PURE__ */ new Set()
2952
+ };
2781
2953
  for (const id of refs.ids) {
2782
- currentContracts.add(id);
2783
- const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
2784
- specs.add(specKey);
2785
- idToSpecs.set(id, specs);
2954
+ current.ids.add(id);
2955
+ const specs = idToSpecs.get(id);
2956
+ if (specs) {
2957
+ specs.add(specKey);
2958
+ }
2786
2959
  }
2787
- specToContractIds.set(specKey, currentContracts);
2960
+ specToContracts.set(specKey, current);
2788
2961
  }
2789
2962
  return {
2790
- specToContractIds,
2963
+ specToContracts,
2791
2964
  idToSpecs,
2792
2965
  missingRefSpecs
2793
2966
  };
@@ -2864,6 +3037,20 @@ function formatList(values) {
2864
3037
  }
2865
3038
  return values.join(", ");
2866
3039
  }
3040
+ function formatMarkdownTable(headers, rows) {
3041
+ const widths = headers.map((header, index) => {
3042
+ const candidates = rows.map((row) => row[index] ?? "");
3043
+ return Math.max(header.length, ...candidates.map((item) => item.length));
3044
+ });
3045
+ const formatRow = (cells) => {
3046
+ const padded = cells.map(
3047
+ (cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)
3048
+ );
3049
+ return `| ${padded.join(" | ")} |`;
3050
+ };
3051
+ const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
3052
+ return [formatRow(headers), separator, ...rows.map(formatRow)];
3053
+ }
2867
3054
  function toSortedArray2(values) {
2868
3055
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2869
3056
  }
@@ -2874,6 +3061,27 @@ function mapToSortedRecord(values) {
2874
3061
  }
2875
3062
  return record2;
2876
3063
  }
3064
+ function mapToSpecContractRecord(values) {
3065
+ const record2 = {};
3066
+ for (const [key, entry] of values.entries()) {
3067
+ record2[key] = {
3068
+ status: entry.status,
3069
+ ids: toSortedArray2(entry.ids)
3070
+ };
3071
+ }
3072
+ return record2;
3073
+ }
3074
+ function normalizeScSources(root, sources) {
3075
+ const normalized = /* @__PURE__ */ new Map();
3076
+ for (const [id, files] of sources.entries()) {
3077
+ const mapped = /* @__PURE__ */ new Set();
3078
+ for (const file of files) {
3079
+ mapped.add(toRelativePath(root, file));
3080
+ }
3081
+ normalized.set(id, mapped);
3082
+ }
3083
+ return normalized;
3084
+ }
2877
3085
  function buildHotspots(issues) {
2878
3086
  const map = /* @__PURE__ */ new Map();
2879
3087
  for (const issue7 of issues) {
@@ -2902,6 +3110,7 @@ function buildHotspots(issues) {
2902
3110
  extractAllIds,
2903
3111
  extractIds,
2904
3112
  extractInvalidIds,
3113
+ findConfigRoot,
2905
3114
  formatReportJson,
2906
3115
  formatReportMarkdown,
2907
3116
  getConfigPath,