qfai 0.5.0 → 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.5.0".length > 0) {
1158
- return "0.5.0";
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,15 +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);
2632
+ const contractIndex = await buildContractIndex(resolvedRoot, config);
2501
2633
  const contractIdList = Array.from(contractIndex.ids);
2502
2634
  const specContractRefs = await collectSpecContractRefs(
2503
2635
  specFiles,
2504
2636
  contractIdList
2505
2637
  );
2506
2638
  const referencedContracts = /* @__PURE__ */ new Set();
2507
- for (const ids of specContractRefs.specToContractIds.values()) {
2508
- ids.forEach((id) => referencedContracts.add(id));
2639
+ for (const entry of specContractRefs.specToContracts.values()) {
2640
+ entry.ids.forEach((id) => referencedContracts.add(id));
2509
2641
  }
2510
2642
  const referencedContractCount = contractIdList.filter(
2511
2643
  (id) => referencedContracts.has(id)
@@ -2514,8 +2646,8 @@ async function createReportData(root, validation, configResult) {
2514
2646
  (id) => !referencedContracts.has(id)
2515
2647
  ).length;
2516
2648
  const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
2517
- const specToContractIdsRecord = mapToSortedRecord(
2518
- specContractRefs.specToContractIds
2649
+ const specToContractsRecord = mapToSpecContractRecord(
2650
+ specContractRefs.specToContracts
2519
2651
  );
2520
2652
  const idsByPrefix = await collectIds([
2521
2653
  ...specFiles,
@@ -2533,24 +2665,26 @@ async function createReportData(root, validation, configResult) {
2533
2665
  srcRoot,
2534
2666
  testsRoot
2535
2667
  );
2536
- const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2537
- const scRefsResult = await collectScTestReferences(
2538
- root,
2539
- config.validation.traceability.testFileGlobs,
2540
- config.validation.traceability.testFileExcludeGlobs
2668
+ const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
2669
+ const normalizedValidation = normalizeValidationResult(
2670
+ resolvedRoot,
2671
+ resolvedValidationRaw
2541
2672
  );
2542
- const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2543
- const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2673
+ const scCoverage = normalizedValidation.traceability.sc;
2674
+ const testFiles = normalizedValidation.traceability.testFiles;
2544
2675
  const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2545
- const scSourceRecord = mapToSortedRecord(scSources);
2546
- const resolvedValidation = validation ?? await validateProject(root, resolved);
2676
+ const scSourceRecord = mapToSortedRecord(
2677
+ normalizeScSources(resolvedRoot, scSources)
2678
+ );
2547
2679
  const version = await resolveToolVersion();
2680
+ const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
2681
+ const displayConfigPath = toRelativePath(resolvedRoot, configPath);
2548
2682
  return {
2549
2683
  tool: "qfai",
2550
2684
  version,
2551
2685
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2552
- root,
2553
- configPath,
2686
+ root: displayRoot,
2687
+ configPath: displayConfigPath,
2554
2688
  summary: {
2555
2689
  specs: specFiles.length,
2556
2690
  scenarios: scenarioFiles.length,
@@ -2559,7 +2693,7 @@ async function createReportData(root, validation, configResult) {
2559
2693
  ui: uiFiles.length,
2560
2694
  db: dbFiles.length
2561
2695
  },
2562
- counts: resolvedValidation.counts
2696
+ counts: normalizedValidation.counts
2563
2697
  },
2564
2698
  ids: {
2565
2699
  spec: idsByPrefix.SPEC,
@@ -2584,21 +2718,23 @@ async function createReportData(root, validation, configResult) {
2584
2718
  specs: {
2585
2719
  contractRefMissing: specContractRefs.missingRefSpecs.size,
2586
2720
  missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
2587
- specToContractIds: specToContractIdsRecord
2721
+ specToContracts: specToContractsRecord
2588
2722
  }
2589
2723
  },
2590
- issues: resolvedValidation.issues
2724
+ issues: normalizedValidation.issues
2591
2725
  };
2592
2726
  }
2593
2727
  function formatReportMarkdown(data) {
2594
2728
  const lines = [];
2595
2729
  lines.push("# QFAI Report");
2730
+ lines.push("");
2596
2731
  lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
2597
2732
  lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
2598
2733
  lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
2599
2734
  lines.push(`- \u7248: ${data.version}`);
2600
2735
  lines.push("");
2601
2736
  lines.push("## \u6982\u8981");
2737
+ lines.push("");
2602
2738
  lines.push(`- specs: ${data.summary.specs}`);
2603
2739
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2604
2740
  lines.push(
@@ -2609,6 +2745,7 @@ function formatReportMarkdown(data) {
2609
2745
  );
2610
2746
  lines.push("");
2611
2747
  lines.push("## ID\u96C6\u8A08");
2748
+ lines.push("");
2612
2749
  lines.push(formatIdLine("SPEC", data.ids.spec));
2613
2750
  lines.push(formatIdLine("BR", data.ids.br));
2614
2751
  lines.push(formatIdLine("SC", data.ids.sc));
@@ -2617,12 +2754,14 @@ function formatReportMarkdown(data) {
2617
2754
  lines.push(formatIdLine("DB", data.ids.db));
2618
2755
  lines.push("");
2619
2756
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
2757
+ lines.push("");
2620
2758
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
2621
2759
  lines.push(
2622
2760
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2623
2761
  );
2624
2762
  lines.push("");
2625
2763
  lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
2764
+ lines.push("");
2626
2765
  lines.push(`- total: ${data.traceability.contracts.total}`);
2627
2766
  lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
2628
2767
  lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
@@ -2631,6 +2770,7 @@ function formatReportMarkdown(data) {
2631
2770
  );
2632
2771
  lines.push("");
2633
2772
  lines.push("## \u5951\u7D04\u2192Spec");
2773
+ lines.push("");
2634
2774
  const contractToSpecs = data.traceability.contracts.idToSpecs;
2635
2775
  const contractIds = Object.keys(contractToSpecs).sort(
2636
2776
  (a, b) => a.localeCompare(b)
@@ -2649,24 +2789,25 @@ function formatReportMarkdown(data) {
2649
2789
  }
2650
2790
  lines.push("");
2651
2791
  lines.push("## Spec\u2192\u5951\u7D04");
2652
- const specToContracts = data.traceability.specs.specToContractIds;
2792
+ lines.push("");
2793
+ const specToContracts = data.traceability.specs.specToContracts;
2653
2794
  const specIds = Object.keys(specToContracts).sort(
2654
2795
  (a, b) => a.localeCompare(b)
2655
2796
  );
2656
2797
  if (specIds.length === 0) {
2657
2798
  lines.push("- (none)");
2658
2799
  } else {
2659
- for (const specId of specIds) {
2660
- const contractIds2 = specToContracts[specId] ?? [];
2661
- if (contractIds2.length === 0) {
2662
- lines.push(`- ${specId}: (none)`);
2663
- } else {
2664
- lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
2665
- }
2666
- }
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));
2667
2807
  }
2668
2808
  lines.push("");
2669
2809
  lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
2810
+ lines.push("");
2670
2811
  const missingRefSpecs = data.traceability.specs.missingRefSpecs;
2671
2812
  if (missingRefSpecs.length === 0) {
2672
2813
  lines.push("- (none)");
@@ -2677,6 +2818,7 @@ function formatReportMarkdown(data) {
2677
2818
  }
2678
2819
  lines.push("");
2679
2820
  lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2821
+ lines.push("");
2680
2822
  lines.push(`- total: ${data.traceability.sc.total}`);
2681
2823
  lines.push(`- covered: ${data.traceability.sc.covered}`);
2682
2824
  lines.push(`- missing: ${data.traceability.sc.missing}`);
@@ -2706,6 +2848,7 @@ function formatReportMarkdown(data) {
2706
2848
  }
2707
2849
  lines.push("");
2708
2850
  lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
2851
+ lines.push("");
2709
2852
  const scRefs = data.traceability.sc.refs;
2710
2853
  const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2711
2854
  if (scIds.length === 0) {
@@ -2722,6 +2865,7 @@ function formatReportMarkdown(data) {
2722
2865
  }
2723
2866
  lines.push("");
2724
2867
  lines.push("## Spec:SC=1:1 \u9055\u53CD");
2868
+ lines.push("");
2725
2869
  const specScIssues = data.issues.filter(
2726
2870
  (item) => item.code === "QFAI-TRACE-012"
2727
2871
  );
@@ -2736,6 +2880,7 @@ function formatReportMarkdown(data) {
2736
2880
  }
2737
2881
  lines.push("");
2738
2882
  lines.push("## Hotspots");
2883
+ lines.push("");
2739
2884
  const hotspots = buildHotspots(data.issues);
2740
2885
  if (hotspots.length === 0) {
2741
2886
  lines.push("- (none)");
@@ -2748,6 +2893,7 @@ function formatReportMarkdown(data) {
2748
2893
  }
2749
2894
  lines.push("");
2750
2895
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
2896
+ lines.push("");
2751
2897
  const traceIssues = data.issues.filter(
2752
2898
  (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
2753
2899
  );
@@ -2763,6 +2909,7 @@ function formatReportMarkdown(data) {
2763
2909
  }
2764
2910
  lines.push("");
2765
2911
  lines.push("## \u691C\u8A3C\u7D50\u679C");
2912
+ lines.push("");
2766
2913
  if (data.issues.length === 0) {
2767
2914
  lines.push("- (none)");
2768
2915
  } else {
@@ -2780,7 +2927,7 @@ function formatReportJson(data) {
2780
2927
  return JSON.stringify(data, null, 2);
2781
2928
  }
2782
2929
  async function collectSpecContractRefs(specFiles, contractIdList) {
2783
- const specToContractIds = /* @__PURE__ */ new Map();
2930
+ const specToContracts = /* @__PURE__ */ new Map();
2784
2931
  const idToSpecs = /* @__PURE__ */ new Map();
2785
2932
  const missingRefSpecs = /* @__PURE__ */ new Set();
2786
2933
  for (const contractId of contractIdList) {
@@ -2789,24 +2936,31 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
2789
2936
  for (const file of specFiles) {
2790
2937
  const text = await (0, import_promises14.readFile)(file, "utf-8");
2791
2938
  const parsed = parseSpec(text, file);
2792
- const specKey = parsed.specId ?? file;
2939
+ const specKey = parsed.specId;
2940
+ if (!specKey) {
2941
+ continue;
2942
+ }
2793
2943
  const refs = parsed.contractRefs;
2794
2944
  if (refs.lines.length === 0) {
2795
2945
  missingRefSpecs.add(specKey);
2946
+ specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
2796
2947
  continue;
2797
2948
  }
2798
- const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
2949
+ const current = specToContracts.get(specKey) ?? {
2950
+ status: "declared",
2951
+ ids: /* @__PURE__ */ new Set()
2952
+ };
2799
2953
  for (const id of refs.ids) {
2800
- currentContracts.add(id);
2954
+ current.ids.add(id);
2801
2955
  const specs = idToSpecs.get(id);
2802
2956
  if (specs) {
2803
2957
  specs.add(specKey);
2804
2958
  }
2805
2959
  }
2806
- specToContractIds.set(specKey, currentContracts);
2960
+ specToContracts.set(specKey, current);
2807
2961
  }
2808
2962
  return {
2809
- specToContractIds,
2963
+ specToContracts,
2810
2964
  idToSpecs,
2811
2965
  missingRefSpecs
2812
2966
  };
@@ -2883,6 +3037,20 @@ function formatList(values) {
2883
3037
  }
2884
3038
  return values.join(", ");
2885
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
+ }
2886
3054
  function toSortedArray2(values) {
2887
3055
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2888
3056
  }
@@ -2893,6 +3061,27 @@ function mapToSortedRecord(values) {
2893
3061
  }
2894
3062
  return record2;
2895
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
+ }
2896
3085
  function buildHotspots(issues) {
2897
3086
  const map = /* @__PURE__ */ new Map();
2898
3087
  for (const issue7 of issues) {
@@ -2921,6 +3110,7 @@ function buildHotspots(issues) {
2921
3110
  extractAllIds,
2922
3111
  extractIds,
2923
3112
  extractInvalidIds,
3113
+ findConfigRoot,
2924
3114
  formatReportJson,
2925
3115
  formatReportMarkdown,
2926
3116
  getConfigPath,