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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/core/config.ts
2
- import { readFile } from "fs/promises";
2
+ import { access, readFile } from "fs/promises";
3
3
  import path from "path";
4
4
  import { parse as parseYaml } from "yaml";
5
5
  var defaultConfig = {
@@ -31,7 +31,7 @@ var defaultConfig = {
31
31
  testFileGlobs: [],
32
32
  testFileExcludeGlobs: [],
33
33
  scNoTestSeverity: "error",
34
- allowOrphanContracts: false,
34
+ orphanContractsPolicy: "error",
35
35
  unknownContractIdSeverity: "error"
36
36
  }
37
37
  },
@@ -42,6 +42,26 @@ var defaultConfig = {
42
42
  function getConfigPath(root) {
43
43
  return path.join(root, "qfai.config.yaml");
44
44
  }
45
+ async function findConfigRoot(startDir) {
46
+ const resolvedStart = path.resolve(startDir);
47
+ let current = resolvedStart;
48
+ while (true) {
49
+ const configPath = getConfigPath(current);
50
+ if (await exists(configPath)) {
51
+ return { root: current, configPath, found: true };
52
+ }
53
+ const parent = path.dirname(current);
54
+ if (parent === current) {
55
+ break;
56
+ }
57
+ current = parent;
58
+ }
59
+ return {
60
+ root: resolvedStart,
61
+ configPath: getConfigPath(resolvedStart),
62
+ found: false
63
+ };
64
+ }
45
65
  async function loadConfig(root) {
46
66
  const configPath = getConfigPath(root);
47
67
  const issues = [];
@@ -231,10 +251,10 @@ function normalizeValidation(raw, configPath, issues) {
231
251
  configPath,
232
252
  issues
233
253
  ),
234
- allowOrphanContracts: readBoolean(
235
- traceabilityRaw?.allowOrphanContracts,
236
- base.traceability.allowOrphanContracts,
237
- "validation.traceability.allowOrphanContracts",
254
+ orphanContractsPolicy: readOrphanContractsPolicy(
255
+ traceabilityRaw?.orphanContractsPolicy,
256
+ base.traceability.orphanContractsPolicy,
257
+ "validation.traceability.orphanContractsPolicy",
238
258
  configPath,
239
259
  issues
240
260
  ),
@@ -330,6 +350,20 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
330
350
  }
331
351
  return fallback;
332
352
  }
353
+ function readOrphanContractsPolicy(value, fallback, label, configPath, issues) {
354
+ if (value === "error" || value === "warning" || value === "allow") {
355
+ return value;
356
+ }
357
+ if (value !== void 0) {
358
+ issues.push(
359
+ configIssue(
360
+ configPath,
361
+ `${label} \u306F error|warning|allow \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
362
+ )
363
+ );
364
+ }
365
+ return fallback;
366
+ }
333
367
  function configIssue(file, message) {
334
368
  return {
335
369
  code: "QFAI_CONFIG_INVALID",
@@ -345,6 +379,14 @@ function isMissingFile(error) {
345
379
  }
346
380
  return false;
347
381
  }
382
+ async function exists(target) {
383
+ try {
384
+ await access(target);
385
+ return true;
386
+ } catch {
387
+ return false;
388
+ }
389
+ }
348
390
  function formatError(error) {
349
391
  if (error instanceof Error) {
350
392
  return error.message;
@@ -410,17 +452,17 @@ function isValidId(value, prefix) {
410
452
 
411
453
  // src/core/report.ts
412
454
  import { readFile as readFile11 } from "fs/promises";
413
- import path11 from "path";
455
+ import path12 from "path";
414
456
 
415
457
  // src/core/contractIndex.ts
416
458
  import { readFile as readFile2 } from "fs/promises";
417
459
  import path4 from "path";
418
460
 
419
461
  // src/core/discovery.ts
420
- import { access as access2 } from "fs/promises";
462
+ import { access as access3 } from "fs/promises";
421
463
 
422
464
  // src/core/fs.ts
423
- import { access, readdir } from "fs/promises";
465
+ import { access as access2, readdir } from "fs/promises";
424
466
  import path2 from "path";
425
467
  import fg from "fast-glob";
426
468
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
@@ -433,7 +475,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
433
475
  ]);
434
476
  async function collectFiles(root, options = {}) {
435
477
  const entries = [];
436
- if (!await exists(root)) {
478
+ if (!await exists2(root)) {
437
479
  return entries;
438
480
  }
439
481
  const ignoreDirs = /* @__PURE__ */ new Set([
@@ -478,9 +520,9 @@ async function walk(base, current, ignoreDirs, extensions, out) {
478
520
  }
479
521
  }
480
522
  }
481
- async function exists(target) {
523
+ async function exists2(target) {
482
524
  try {
483
- await access(target);
525
+ await access2(target);
484
526
  return true;
485
527
  } catch {
486
528
  return false;
@@ -552,15 +594,15 @@ async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
552
594
  async function filterExisting(files) {
553
595
  const existing = [];
554
596
  for (const file of files) {
555
- if (await exists2(file)) {
597
+ if (await exists3(file)) {
556
598
  existing.push(file);
557
599
  }
558
600
  }
559
601
  return existing;
560
602
  }
561
- async function exists2(target) {
603
+ async function exists3(target) {
562
604
  try {
563
- await access2(target);
605
+ await access3(target);
564
606
  return true;
565
607
  } catch {
566
608
  return false;
@@ -618,6 +660,113 @@ function record(index, id, file) {
618
660
  index.idToFiles.set(id, current);
619
661
  }
620
662
 
663
+ // src/core/paths.ts
664
+ import path5 from "path";
665
+ function toRelativePath(root, target) {
666
+ if (!target) {
667
+ return target;
668
+ }
669
+ if (!path5.isAbsolute(target)) {
670
+ return toPosixPath(target);
671
+ }
672
+ const relative = path5.relative(root, target);
673
+ if (!relative) {
674
+ return ".";
675
+ }
676
+ return toPosixPath(relative);
677
+ }
678
+ function toPosixPath(value) {
679
+ return value.replace(/\\/g, "/");
680
+ }
681
+
682
+ // src/core/normalize.ts
683
+ function normalizeIssuePaths(root, issues) {
684
+ return issues.map((issue7) => {
685
+ if (!issue7.file) {
686
+ return issue7;
687
+ }
688
+ const normalized = toRelativePath(root, issue7.file);
689
+ if (normalized === issue7.file) {
690
+ return issue7;
691
+ }
692
+ return {
693
+ ...issue7,
694
+ file: normalized
695
+ };
696
+ });
697
+ }
698
+ function normalizeScCoverage(root, sc) {
699
+ const refs = {};
700
+ for (const [scId, files] of Object.entries(sc.refs)) {
701
+ refs[scId] = files.map((file) => toRelativePath(root, file));
702
+ }
703
+ return {
704
+ ...sc,
705
+ refs
706
+ };
707
+ }
708
+ function normalizeValidationResult(root, result) {
709
+ return {
710
+ ...result,
711
+ issues: normalizeIssuePaths(root, result.issues),
712
+ traceability: {
713
+ ...result.traceability,
714
+ sc: normalizeScCoverage(root, result.traceability.sc)
715
+ }
716
+ };
717
+ }
718
+
719
+ // src/core/parse/contractRefs.ts
720
+ var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
721
+ function parseContractRefs(text, options = {}) {
722
+ const linePattern = buildLinePattern(options);
723
+ const lines = [];
724
+ for (const match of text.matchAll(linePattern)) {
725
+ lines.push((match[1] ?? "").trim());
726
+ }
727
+ const ids = [];
728
+ const invalidTokens = [];
729
+ let hasNone = false;
730
+ for (const line of lines) {
731
+ if (line.length === 0) {
732
+ invalidTokens.push("(empty)");
733
+ continue;
734
+ }
735
+ const tokens = line.split(",").map((token) => token.trim());
736
+ for (const token of tokens) {
737
+ if (token.length === 0) {
738
+ invalidTokens.push("(empty)");
739
+ continue;
740
+ }
741
+ if (token === "none") {
742
+ hasNone = true;
743
+ continue;
744
+ }
745
+ if (CONTRACT_REF_ID_RE.test(token)) {
746
+ ids.push(token);
747
+ continue;
748
+ }
749
+ invalidTokens.push(token);
750
+ }
751
+ }
752
+ return {
753
+ lines,
754
+ ids: unique2(ids),
755
+ invalidTokens: unique2(invalidTokens),
756
+ hasNone
757
+ };
758
+ }
759
+ function buildLinePattern(options) {
760
+ const prefix = options.allowCommentPrefix ? "#" : "";
761
+ return new RegExp(
762
+ `^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
763
+ "gm"
764
+ );
765
+ }
766
+ function unique2(values) {
767
+ return Array.from(new Set(values));
768
+ }
769
+
621
770
  // src/core/parse/markdown.ts
622
771
  var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
623
772
  function parseHeadings(md) {
@@ -664,8 +813,6 @@ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
664
813
  var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
665
814
  var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
666
815
  var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
667
- var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
668
- var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
669
816
  var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
670
817
  var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
671
818
  function parseSpec(md, file) {
@@ -738,50 +885,10 @@ function parseSpec(md, file) {
738
885
  }
739
886
  return parsed;
740
887
  }
741
- function parseContractRefs(md) {
742
- const lines = [];
743
- for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
744
- lines.push((match[1] ?? "").trim());
745
- }
746
- const ids = [];
747
- const invalidTokens = [];
748
- let hasNone = false;
749
- for (const line of lines) {
750
- if (line.length === 0) {
751
- invalidTokens.push("(empty)");
752
- continue;
753
- }
754
- const tokens = line.split(",").map((token) => token.trim());
755
- for (const token of tokens) {
756
- if (token.length === 0) {
757
- invalidTokens.push("(empty)");
758
- continue;
759
- }
760
- if (token === "none") {
761
- hasNone = true;
762
- continue;
763
- }
764
- if (CONTRACT_REF_ID_RE.test(token)) {
765
- ids.push(token);
766
- continue;
767
- }
768
- invalidTokens.push(token);
769
- }
770
- }
771
- return {
772
- lines,
773
- ids: unique2(ids),
774
- invalidTokens: unique2(invalidTokens),
775
- hasNone
776
- };
777
- }
778
- function unique2(values) {
779
- return Array.from(new Set(values));
780
- }
781
888
 
782
889
  // src/core/traceability.ts
783
890
  import { readFile as readFile3 } from "fs/promises";
784
- import path5 from "path";
891
+ import path6 from "path";
785
892
 
786
893
  // src/core/gherkin/parse.ts
787
894
  import {
@@ -816,9 +923,6 @@ function formatError2(error) {
816
923
  var SPEC_TAG_RE = /^SPEC-\d{4}$/;
817
924
  var SC_TAG_RE = /^SC-\d{4}$/;
818
925
  var BR_TAG_RE = /^BR-\d{4}$/;
819
- var UI_TAG_RE = /^UI-\d{4}$/;
820
- var API_TAG_RE = /^API-\d{4}$/;
821
- var DB_TAG_RE = /^DB-\d{4}$/;
822
926
  function parseScenarioDocument(text, uri) {
823
927
  const { gherkinDocument, errors } = parseGherkin(text, uri);
824
928
  if (!gherkinDocument) {
@@ -843,31 +947,21 @@ function parseScenarioDocument(text, uri) {
843
947
  errors
844
948
  };
845
949
  }
846
- function buildScenarioAtoms(document) {
950
+ function buildScenarioAtoms(document, contractIds = []) {
951
+ const uniqueContractIds = unique3(contractIds).sort(
952
+ (a, b) => a.localeCompare(b)
953
+ );
847
954
  return document.scenarios.map((scenario) => {
848
955
  const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
849
956
  const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
850
957
  const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
851
- const contractIds = /* @__PURE__ */ new Set();
852
- scenario.tags.forEach((tag) => {
853
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
854
- contractIds.add(tag);
855
- }
856
- });
857
- for (const step of scenario.steps) {
858
- for (const text of collectStepTexts(step)) {
859
- extractIds(text, "UI").forEach((id) => contractIds.add(id));
860
- extractIds(text, "API").forEach((id) => contractIds.add(id));
861
- extractIds(text, "DB").forEach((id) => contractIds.add(id));
862
- }
863
- }
864
958
  const atom = {
865
959
  uri: document.uri,
866
960
  featureName: document.featureName ?? "",
867
961
  scenarioName: scenario.name,
868
962
  kind: scenario.kind,
869
963
  brIds,
870
- contractIds: Array.from(contractIds).sort()
964
+ contractIds: uniqueContractIds
871
965
  };
872
966
  if (scenario.line !== void 0) {
873
967
  atom.line = scenario.line;
@@ -920,23 +1014,6 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
920
1014
  function collectTagNames(tags) {
921
1015
  return tags.map((tag) => tag.name.replace(/^@/, ""));
922
1016
  }
923
- function collectStepTexts(step) {
924
- const texts = [];
925
- if (step.text) {
926
- texts.push(step.text);
927
- }
928
- if (step.docString?.content) {
929
- texts.push(step.docString.content);
930
- }
931
- if (step.dataTable?.rows) {
932
- for (const row of step.dataTable.rows) {
933
- for (const cell of row.cells) {
934
- texts.push(cell.value);
935
- }
936
- }
937
- }
938
- return texts;
939
- }
940
1017
  function unique3(values) {
941
1018
  return Array.from(new Set(values));
942
1019
  }
@@ -1038,7 +1115,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1038
1115
  };
1039
1116
  }
1040
1117
  const normalizedFiles = Array.from(
1041
- new Set(files.map((file) => path5.normalize(file)))
1118
+ new Set(files.map((file) => path6.normalize(file)))
1042
1119
  );
1043
1120
  for (const file of normalizedFiles) {
1044
1121
  const text = await readFile3(file, "utf-8");
@@ -1099,11 +1176,11 @@ function formatError3(error) {
1099
1176
 
1100
1177
  // src/core/version.ts
1101
1178
  import { readFile as readFile4 } from "fs/promises";
1102
- import path6 from "path";
1179
+ import path7 from "path";
1103
1180
  import { fileURLToPath } from "url";
1104
1181
  async function resolveToolVersion() {
1105
- if ("0.4.9".length > 0) {
1106
- return "0.4.9";
1182
+ if ("0.5.2".length > 0) {
1183
+ return "0.5.2";
1107
1184
  }
1108
1185
  try {
1109
1186
  const packagePath = resolvePackageJsonPath();
@@ -1118,18 +1195,18 @@ async function resolveToolVersion() {
1118
1195
  function resolvePackageJsonPath() {
1119
1196
  const base = import.meta.url;
1120
1197
  const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
1121
- return path6.resolve(path6.dirname(basePath), "../../package.json");
1198
+ return path7.resolve(path7.dirname(basePath), "../../package.json");
1122
1199
  }
1123
1200
 
1124
1201
  // src/core/validators/contracts.ts
1125
1202
  import { readFile as readFile5 } from "fs/promises";
1126
- import path8 from "path";
1203
+ import path9 from "path";
1127
1204
 
1128
1205
  // src/core/contracts.ts
1129
- import path7 from "path";
1206
+ import path8 from "path";
1130
1207
  import { parse as parseYaml2 } from "yaml";
1131
1208
  function parseStructuredContract(file, text) {
1132
- const ext = path7.extname(file).toLowerCase();
1209
+ const ext = path8.extname(file).toLowerCase();
1133
1210
  if (ext === ".json") {
1134
1211
  return JSON.parse(text);
1135
1212
  }
@@ -1149,9 +1226,9 @@ var SQL_DANGEROUS_PATTERNS = [
1149
1226
  async function validateContracts(root, config) {
1150
1227
  const issues = [];
1151
1228
  const contractsRoot = resolvePath(root, config, "contractsDir");
1152
- issues.push(...await validateUiContracts(path8.join(contractsRoot, "ui")));
1153
- issues.push(...await validateApiContracts(path8.join(contractsRoot, "api")));
1154
- issues.push(...await validateDbContracts(path8.join(contractsRoot, "db")));
1229
+ issues.push(...await validateUiContracts(path9.join(contractsRoot, "ui")));
1230
+ issues.push(...await validateApiContracts(path9.join(contractsRoot, "api")));
1231
+ issues.push(...await validateDbContracts(path9.join(contractsRoot, "db")));
1155
1232
  const contractIndex = await buildContractIndex(root, config);
1156
1233
  issues.push(...validateDuplicateContractIds(contractIndex));
1157
1234
  return issues;
@@ -1434,7 +1511,7 @@ function issue(code, message, severity, file, rule, refs) {
1434
1511
 
1435
1512
  // src/core/validators/delta.ts
1436
1513
  import { readFile as readFile6 } from "fs/promises";
1437
- import path9 from "path";
1514
+ import path10 from "path";
1438
1515
  var SECTION_RE = /^##\s+変更区分/m;
1439
1516
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1440
1517
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1448,7 +1525,7 @@ async function validateDeltas(root, config) {
1448
1525
  }
1449
1526
  const issues = [];
1450
1527
  for (const pack of packs) {
1451
- const deltaPath = path9.join(pack, "delta.md");
1528
+ const deltaPath = path10.join(pack, "delta.md");
1452
1529
  let text;
1453
1530
  try {
1454
1531
  text = await readFile6(deltaPath, "utf-8");
@@ -1524,7 +1601,7 @@ function issue2(code, message, severity, file, rule, refs) {
1524
1601
 
1525
1602
  // src/core/validators/ids.ts
1526
1603
  import { readFile as readFile7 } from "fs/promises";
1527
- import path10 from "path";
1604
+ import path11 from "path";
1528
1605
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1529
1606
  async function validateDefinedIds(root, config) {
1530
1607
  const issues = [];
@@ -1590,7 +1667,7 @@ function recordId(out, id, file) {
1590
1667
  }
1591
1668
  function formatFileList(files, root) {
1592
1669
  return files.map((file) => {
1593
- const relative = path10.relative(root, file);
1670
+ const relative = path11.relative(root, file);
1594
1671
  return relative.length > 0 ? relative : file;
1595
1672
  }).join(", ");
1596
1673
  }
@@ -2027,7 +2104,7 @@ async function validateTraceability(root, config) {
2027
2104
  if (contractRefs.hasNone && contractRefs.ids.length > 0) {
2028
2105
  issues.push(
2029
2106
  issue6(
2030
- "QFAI-TRACE-021",
2107
+ "QFAI-TRACE-023",
2031
2108
  "Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2032
2109
  "error",
2033
2110
  file,
@@ -2059,7 +2136,7 @@ async function validateTraceability(root, config) {
2059
2136
  if (unknownContractIds.length > 0) {
2060
2137
  issues.push(
2061
2138
  issue6(
2062
- "QFAI-TRACE-021",
2139
+ "QFAI-TRACE-024",
2063
2140
  `Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2064
2141
  ", "
2065
2142
  )}`,
@@ -2074,11 +2151,62 @@ async function validateTraceability(root, config) {
2074
2151
  for (const file of scenarioFiles) {
2075
2152
  const text = await readFile10(file, "utf-8");
2076
2153
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2154
+ const scenarioContractRefs = parseContractRefs(text, {
2155
+ allowCommentPrefix: true
2156
+ });
2157
+ if (scenarioContractRefs.lines.length === 0) {
2158
+ issues.push(
2159
+ issue6(
2160
+ "QFAI-TRACE-031",
2161
+ "Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2162
+ "error",
2163
+ file,
2164
+ "traceability.scenarioContractRefRequired"
2165
+ )
2166
+ );
2167
+ } else {
2168
+ if (scenarioContractRefs.hasNone && scenarioContractRefs.ids.length > 0) {
2169
+ issues.push(
2170
+ issue6(
2171
+ "QFAI-TRACE-033",
2172
+ "Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2173
+ "error",
2174
+ file,
2175
+ "traceability.scenarioContractRefFormat"
2176
+ )
2177
+ );
2178
+ }
2179
+ if (scenarioContractRefs.invalidTokens.length > 0) {
2180
+ issues.push(
2181
+ issue6(
2182
+ "QFAI-TRACE-032",
2183
+ `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
2184
+ ", "
2185
+ )}`,
2186
+ "error",
2187
+ file,
2188
+ "traceability.scenarioContractRefFormat",
2189
+ scenarioContractRefs.invalidTokens
2190
+ )
2191
+ );
2192
+ }
2193
+ }
2077
2194
  const { document, errors } = parseScenarioDocument(text, file);
2078
2195
  if (!document || errors.length > 0) {
2079
2196
  continue;
2080
2197
  }
2081
- const atoms = buildScenarioAtoms(document);
2198
+ if (document.scenarios.length !== 1) {
2199
+ issues.push(
2200
+ issue6(
2201
+ "QFAI-TRACE-030",
2202
+ `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})`,
2203
+ "error",
2204
+ file,
2205
+ "traceability.scenarioOnePerFile"
2206
+ )
2207
+ );
2208
+ }
2209
+ const atoms = buildScenarioAtoms(document, scenarioContractRefs.ids);
2082
2210
  const scIdsInFile = /* @__PURE__ */ new Set();
2083
2211
  for (const [index, scenario] of document.scenarios.entries()) {
2084
2212
  const atom = atoms[index];
@@ -2223,7 +2351,7 @@ async function validateTraceability(root, config) {
2223
2351
  if (orphanBrIds.length > 0) {
2224
2352
  issues.push(
2225
2353
  issue6(
2226
- "QFAI_TRACE_BR_ORPHAN",
2354
+ "QFAI-TRACE-009",
2227
2355
  `BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
2228
2356
  "error",
2229
2357
  specsRoot,
@@ -2293,17 +2421,19 @@ async function validateTraceability(root, config) {
2293
2421
  );
2294
2422
  }
2295
2423
  }
2296
- if (!config.validation.traceability.allowOrphanContracts) {
2424
+ const orphanPolicy = config.validation.traceability.orphanContractsPolicy;
2425
+ if (orphanPolicy !== "allow") {
2297
2426
  if (contractIds.size > 0) {
2298
2427
  const orphanContracts = Array.from(contractIds).filter(
2299
2428
  (id) => !specContractIds.has(id)
2300
2429
  );
2301
2430
  if (orphanContracts.length > 0) {
2431
+ const severity = orphanPolicy === "warning" ? "warning" : "error";
2302
2432
  issues.push(
2303
2433
  issue6(
2304
2434
  "QFAI-TRACE-022",
2305
2435
  `\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2306
- "error",
2436
+ severity,
2307
2437
  specsRoot,
2308
2438
  "traceability.contractCoverage",
2309
2439
  orphanContracts
@@ -2428,16 +2558,17 @@ function countIssues(issues) {
2428
2558
  // src/core/report.ts
2429
2559
  var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2430
2560
  async function createReportData(root, validation, configResult) {
2431
- const resolved = configResult ?? await loadConfig(root);
2561
+ const resolvedRoot = path12.resolve(root);
2562
+ const resolved = configResult ?? await loadConfig(resolvedRoot);
2432
2563
  const config = resolved.config;
2433
2564
  const configPath = resolved.configPath;
2434
- const specsRoot = resolvePath(root, config, "specsDir");
2435
- const contractsRoot = resolvePath(root, config, "contractsDir");
2436
- const apiRoot = path11.join(contractsRoot, "api");
2437
- const uiRoot = path11.join(contractsRoot, "ui");
2438
- const dbRoot = path11.join(contractsRoot, "db");
2439
- const srcRoot = resolvePath(root, config, "srcDir");
2440
- const testsRoot = resolvePath(root, config, "testsDir");
2565
+ const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
2566
+ const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
2567
+ const apiRoot = path12.join(contractsRoot, "api");
2568
+ const uiRoot = path12.join(contractsRoot, "ui");
2569
+ const dbRoot = path12.join(contractsRoot, "db");
2570
+ const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
2571
+ const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
2441
2572
  const specFiles = await collectSpecFiles(specsRoot);
2442
2573
  const scenarioFiles = await collectScenarioFiles(specsRoot);
2443
2574
  const {
@@ -2445,12 +2576,15 @@ async function createReportData(root, validation, configResult) {
2445
2576
  ui: uiFiles,
2446
2577
  db: dbFiles
2447
2578
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
2448
- const contractIndex = await buildContractIndex(root, config);
2449
- const specContractRefs = await collectSpecContractRefs(specFiles);
2579
+ const contractIndex = await buildContractIndex(resolvedRoot, config);
2450
2580
  const contractIdList = Array.from(contractIndex.ids);
2581
+ const specContractRefs = await collectSpecContractRefs(
2582
+ specFiles,
2583
+ contractIdList
2584
+ );
2451
2585
  const referencedContracts = /* @__PURE__ */ new Set();
2452
- for (const ids of specContractRefs.specToContractIds.values()) {
2453
- ids.forEach((id) => referencedContracts.add(id));
2586
+ for (const entry of specContractRefs.specToContracts.values()) {
2587
+ entry.ids.forEach((id) => referencedContracts.add(id));
2454
2588
  }
2455
2589
  const referencedContractCount = contractIdList.filter(
2456
2590
  (id) => referencedContracts.has(id)
@@ -2459,8 +2593,8 @@ async function createReportData(root, validation, configResult) {
2459
2593
  (id) => !referencedContracts.has(id)
2460
2594
  ).length;
2461
2595
  const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
2462
- const specToContractIdsRecord = mapToSortedRecord(
2463
- specContractRefs.specToContractIds
2596
+ const specToContractsRecord = mapToSpecContractRecord(
2597
+ specContractRefs.specToContracts
2464
2598
  );
2465
2599
  const idsByPrefix = await collectIds([
2466
2600
  ...specFiles,
@@ -2478,24 +2612,26 @@ async function createReportData(root, validation, configResult) {
2478
2612
  srcRoot,
2479
2613
  testsRoot
2480
2614
  );
2481
- const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2482
- const scRefsResult = await collectScTestReferences(
2483
- root,
2484
- config.validation.traceability.testFileGlobs,
2485
- config.validation.traceability.testFileExcludeGlobs
2615
+ const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
2616
+ const normalizedValidation = normalizeValidationResult(
2617
+ resolvedRoot,
2618
+ resolvedValidationRaw
2486
2619
  );
2487
- const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2488
- const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2620
+ const scCoverage = normalizedValidation.traceability.sc;
2621
+ const testFiles = normalizedValidation.traceability.testFiles;
2489
2622
  const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2490
- const scSourceRecord = mapToSortedRecord(scSources);
2491
- const resolvedValidation = validation ?? await validateProject(root, resolved);
2623
+ const scSourceRecord = mapToSortedRecord(
2624
+ normalizeScSources(resolvedRoot, scSources)
2625
+ );
2492
2626
  const version = await resolveToolVersion();
2627
+ const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
2628
+ const displayConfigPath = toRelativePath(resolvedRoot, configPath);
2493
2629
  return {
2494
2630
  tool: "qfai",
2495
2631
  version,
2496
2632
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2497
- root,
2498
- configPath,
2633
+ root: displayRoot,
2634
+ configPath: displayConfigPath,
2499
2635
  summary: {
2500
2636
  specs: specFiles.length,
2501
2637
  scenarios: scenarioFiles.length,
@@ -2504,7 +2640,7 @@ async function createReportData(root, validation, configResult) {
2504
2640
  ui: uiFiles.length,
2505
2641
  db: dbFiles.length
2506
2642
  },
2507
- counts: resolvedValidation.counts
2643
+ counts: normalizedValidation.counts
2508
2644
  },
2509
2645
  ids: {
2510
2646
  spec: idsByPrefix.SPEC,
@@ -2528,21 +2664,24 @@ async function createReportData(root, validation, configResult) {
2528
2664
  },
2529
2665
  specs: {
2530
2666
  contractRefMissing: specContractRefs.missingRefSpecs.size,
2531
- specToContractIds: specToContractIdsRecord
2667
+ missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
2668
+ specToContracts: specToContractsRecord
2532
2669
  }
2533
2670
  },
2534
- issues: resolvedValidation.issues
2671
+ issues: normalizedValidation.issues
2535
2672
  };
2536
2673
  }
2537
2674
  function formatReportMarkdown(data) {
2538
2675
  const lines = [];
2539
2676
  lines.push("# QFAI Report");
2677
+ lines.push("");
2540
2678
  lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
2541
2679
  lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
2542
2680
  lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
2543
2681
  lines.push(`- \u7248: ${data.version}`);
2544
2682
  lines.push("");
2545
2683
  lines.push("## \u6982\u8981");
2684
+ lines.push("");
2546
2685
  lines.push(`- specs: ${data.summary.specs}`);
2547
2686
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2548
2687
  lines.push(
@@ -2553,6 +2692,7 @@ function formatReportMarkdown(data) {
2553
2692
  );
2554
2693
  lines.push("");
2555
2694
  lines.push("## ID\u96C6\u8A08");
2695
+ lines.push("");
2556
2696
  lines.push(formatIdLine("SPEC", data.ids.spec));
2557
2697
  lines.push(formatIdLine("BR", data.ids.br));
2558
2698
  lines.push(formatIdLine("SC", data.ids.sc));
@@ -2561,12 +2701,14 @@ function formatReportMarkdown(data) {
2561
2701
  lines.push(formatIdLine("DB", data.ids.db));
2562
2702
  lines.push("");
2563
2703
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
2704
+ lines.push("");
2564
2705
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
2565
2706
  lines.push(
2566
2707
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2567
2708
  );
2568
2709
  lines.push("");
2569
2710
  lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
2711
+ lines.push("");
2570
2712
  lines.push(`- total: ${data.traceability.contracts.total}`);
2571
2713
  lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
2572
2714
  lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
@@ -2575,6 +2717,7 @@ function formatReportMarkdown(data) {
2575
2717
  );
2576
2718
  lines.push("");
2577
2719
  lines.push("## \u5951\u7D04\u2192Spec");
2720
+ lines.push("");
2578
2721
  const contractToSpecs = data.traceability.contracts.idToSpecs;
2579
2722
  const contractIds = Object.keys(contractToSpecs).sort(
2580
2723
  (a, b) => a.localeCompare(b)
@@ -2593,24 +2736,36 @@ function formatReportMarkdown(data) {
2593
2736
  }
2594
2737
  lines.push("");
2595
2738
  lines.push("## Spec\u2192\u5951\u7D04");
2596
- const specToContracts = data.traceability.specs.specToContractIds;
2739
+ lines.push("");
2740
+ const specToContracts = data.traceability.specs.specToContracts;
2597
2741
  const specIds = Object.keys(specToContracts).sort(
2598
2742
  (a, b) => a.localeCompare(b)
2599
2743
  );
2600
2744
  if (specIds.length === 0) {
2601
2745
  lines.push("- (none)");
2602
2746
  } else {
2603
- for (const specId of specIds) {
2604
- const contractIds2 = specToContracts[specId] ?? [];
2605
- if (contractIds2.length === 0) {
2606
- lines.push(`- ${specId}: (none)`);
2607
- } else {
2608
- lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
2609
- }
2747
+ const rows = specIds.map((specId) => {
2748
+ const entry = specToContracts[specId];
2749
+ const contracts = entry?.status === "missing" ? "(missing)" : entry && entry.ids.length > 0 ? entry.ids.join(", ") : "(none)";
2750
+ const status = entry?.status ?? "missing";
2751
+ return [specId, status, contracts];
2752
+ });
2753
+ lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
2754
+ }
2755
+ lines.push("");
2756
+ lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
2757
+ lines.push("");
2758
+ const missingRefSpecs = data.traceability.specs.missingRefSpecs;
2759
+ if (missingRefSpecs.length === 0) {
2760
+ lines.push("- (none)");
2761
+ } else {
2762
+ for (const specId of missingRefSpecs) {
2763
+ lines.push(`- ${specId}`);
2610
2764
  }
2611
2765
  }
2612
2766
  lines.push("");
2613
2767
  lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2768
+ lines.push("");
2614
2769
  lines.push(`- total: ${data.traceability.sc.total}`);
2615
2770
  lines.push(`- covered: ${data.traceability.sc.covered}`);
2616
2771
  lines.push(`- missing: ${data.traceability.sc.missing}`);
@@ -2640,6 +2795,7 @@ function formatReportMarkdown(data) {
2640
2795
  }
2641
2796
  lines.push("");
2642
2797
  lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
2798
+ lines.push("");
2643
2799
  const scRefs = data.traceability.sc.refs;
2644
2800
  const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2645
2801
  if (scIds.length === 0) {
@@ -2656,6 +2812,7 @@ function formatReportMarkdown(data) {
2656
2812
  }
2657
2813
  lines.push("");
2658
2814
  lines.push("## Spec:SC=1:1 \u9055\u53CD");
2815
+ lines.push("");
2659
2816
  const specScIssues = data.issues.filter(
2660
2817
  (item) => item.code === "QFAI-TRACE-012"
2661
2818
  );
@@ -2670,6 +2827,7 @@ function formatReportMarkdown(data) {
2670
2827
  }
2671
2828
  lines.push("");
2672
2829
  lines.push("## Hotspots");
2830
+ lines.push("");
2673
2831
  const hotspots = buildHotspots(data.issues);
2674
2832
  if (hotspots.length === 0) {
2675
2833
  lines.push("- (none)");
@@ -2682,6 +2840,7 @@ function formatReportMarkdown(data) {
2682
2840
  }
2683
2841
  lines.push("");
2684
2842
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
2843
+ lines.push("");
2685
2844
  const traceIssues = data.issues.filter(
2686
2845
  (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
2687
2846
  );
@@ -2697,6 +2856,7 @@ function formatReportMarkdown(data) {
2697
2856
  }
2698
2857
  lines.push("");
2699
2858
  lines.push("## \u691C\u8A3C\u7D50\u679C");
2859
+ lines.push("");
2700
2860
  if (data.issues.length === 0) {
2701
2861
  lines.push("- (none)");
2702
2862
  } else {
@@ -2713,29 +2873,41 @@ function formatReportMarkdown(data) {
2713
2873
  function formatReportJson(data) {
2714
2874
  return JSON.stringify(data, null, 2);
2715
2875
  }
2716
- async function collectSpecContractRefs(specFiles) {
2717
- const specToContractIds = /* @__PURE__ */ new Map();
2876
+ async function collectSpecContractRefs(specFiles, contractIdList) {
2877
+ const specToContracts = /* @__PURE__ */ new Map();
2718
2878
  const idToSpecs = /* @__PURE__ */ new Map();
2719
2879
  const missingRefSpecs = /* @__PURE__ */ new Set();
2880
+ for (const contractId of contractIdList) {
2881
+ idToSpecs.set(contractId, /* @__PURE__ */ new Set());
2882
+ }
2720
2883
  for (const file of specFiles) {
2721
2884
  const text = await readFile11(file, "utf-8");
2722
2885
  const parsed = parseSpec(text, file);
2723
- const specKey = parsed.specId ?? file;
2886
+ const specKey = parsed.specId;
2887
+ if (!specKey) {
2888
+ continue;
2889
+ }
2724
2890
  const refs = parsed.contractRefs;
2725
2891
  if (refs.lines.length === 0) {
2726
2892
  missingRefSpecs.add(specKey);
2893
+ specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
2894
+ continue;
2727
2895
  }
2728
- const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
2896
+ const current = specToContracts.get(specKey) ?? {
2897
+ status: "declared",
2898
+ ids: /* @__PURE__ */ new Set()
2899
+ };
2729
2900
  for (const id of refs.ids) {
2730
- currentContracts.add(id);
2731
- const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
2732
- specs.add(specKey);
2733
- idToSpecs.set(id, specs);
2901
+ current.ids.add(id);
2902
+ const specs = idToSpecs.get(id);
2903
+ if (specs) {
2904
+ specs.add(specKey);
2905
+ }
2734
2906
  }
2735
- specToContractIds.set(specKey, currentContracts);
2907
+ specToContracts.set(specKey, current);
2736
2908
  }
2737
2909
  return {
2738
- specToContractIds,
2910
+ specToContracts,
2739
2911
  idToSpecs,
2740
2912
  missingRefSpecs
2741
2913
  };
@@ -2812,6 +2984,20 @@ function formatList(values) {
2812
2984
  }
2813
2985
  return values.join(", ");
2814
2986
  }
2987
+ function formatMarkdownTable(headers, rows) {
2988
+ const widths = headers.map((header, index) => {
2989
+ const candidates = rows.map((row) => row[index] ?? "");
2990
+ return Math.max(header.length, ...candidates.map((item) => item.length));
2991
+ });
2992
+ const formatRow = (cells) => {
2993
+ const padded = cells.map(
2994
+ (cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)
2995
+ );
2996
+ return `| ${padded.join(" | ")} |`;
2997
+ };
2998
+ const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
2999
+ return [formatRow(headers), separator, ...rows.map(formatRow)];
3000
+ }
2815
3001
  function toSortedArray2(values) {
2816
3002
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2817
3003
  }
@@ -2822,6 +3008,27 @@ function mapToSortedRecord(values) {
2822
3008
  }
2823
3009
  return record2;
2824
3010
  }
3011
+ function mapToSpecContractRecord(values) {
3012
+ const record2 = {};
3013
+ for (const [key, entry] of values.entries()) {
3014
+ record2[key] = {
3015
+ status: entry.status,
3016
+ ids: toSortedArray2(entry.ids)
3017
+ };
3018
+ }
3019
+ return record2;
3020
+ }
3021
+ function normalizeScSources(root, sources) {
3022
+ const normalized = /* @__PURE__ */ new Map();
3023
+ for (const [id, files] of sources.entries()) {
3024
+ const mapped = /* @__PURE__ */ new Set();
3025
+ for (const file of files) {
3026
+ mapped.add(toRelativePath(root, file));
3027
+ }
3028
+ normalized.set(id, mapped);
3029
+ }
3030
+ return normalized;
3031
+ }
2825
3032
  function buildHotspots(issues) {
2826
3033
  const map = /* @__PURE__ */ new Map();
2827
3034
  for (const issue7 of issues) {
@@ -2849,6 +3056,7 @@ export {
2849
3056
  extractAllIds,
2850
3057
  extractIds,
2851
3058
  extractInvalidIds,
3059
+ findConfigRoot,
2852
3060
  formatReportJson,
2853
3061
  formatReportMarkdown,
2854
3062
  getConfigPath,