qfai 0.5.0 → 0.6.0

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.5.0".length > 0) {
1106
- return "0.5.0";
1182
+ if ("0.6.0".length > 0) {
1183
+ return "0.6.0";
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,15 +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);
2579
+ const contractIndex = await buildContractIndex(resolvedRoot, config);
2449
2580
  const contractIdList = Array.from(contractIndex.ids);
2450
2581
  const specContractRefs = await collectSpecContractRefs(
2451
2582
  specFiles,
2452
2583
  contractIdList
2453
2584
  );
2454
2585
  const referencedContracts = /* @__PURE__ */ new Set();
2455
- for (const ids of specContractRefs.specToContractIds.values()) {
2456
- ids.forEach((id) => referencedContracts.add(id));
2586
+ for (const entry of specContractRefs.specToContracts.values()) {
2587
+ entry.ids.forEach((id) => referencedContracts.add(id));
2457
2588
  }
2458
2589
  const referencedContractCount = contractIdList.filter(
2459
2590
  (id) => referencedContracts.has(id)
@@ -2462,8 +2593,8 @@ async function createReportData(root, validation, configResult) {
2462
2593
  (id) => !referencedContracts.has(id)
2463
2594
  ).length;
2464
2595
  const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
2465
- const specToContractIdsRecord = mapToSortedRecord(
2466
- specContractRefs.specToContractIds
2596
+ const specToContractsRecord = mapToSpecContractRecord(
2597
+ specContractRefs.specToContracts
2467
2598
  );
2468
2599
  const idsByPrefix = await collectIds([
2469
2600
  ...specFiles,
@@ -2481,24 +2612,28 @@ async function createReportData(root, validation, configResult) {
2481
2612
  srcRoot,
2482
2613
  testsRoot
2483
2614
  );
2484
- const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2485
- const scRefsResult = await collectScTestReferences(
2486
- root,
2487
- config.validation.traceability.testFileGlobs,
2488
- config.validation.traceability.testFileExcludeGlobs
2615
+ const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
2616
+ const normalizedValidation = normalizeValidationResult(
2617
+ resolvedRoot,
2618
+ resolvedValidationRaw
2489
2619
  );
2490
- const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2491
- const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2620
+ const scCoverage = normalizedValidation.traceability.sc;
2621
+ const testFiles = normalizedValidation.traceability.testFiles;
2492
2622
  const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2493
- const scSourceRecord = mapToSortedRecord(scSources);
2494
- const resolvedValidation = validation ?? await validateProject(root, resolved);
2623
+ const scSourceRecord = mapToSortedRecord(
2624
+ normalizeScSources(resolvedRoot, scSources)
2625
+ );
2495
2626
  const version = await resolveToolVersion();
2627
+ const reportFormatVersion = 1;
2628
+ const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
2629
+ const displayConfigPath = toRelativePath(resolvedRoot, configPath);
2496
2630
  return {
2497
2631
  tool: "qfai",
2498
2632
  version,
2633
+ reportFormatVersion,
2499
2634
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2500
- root,
2501
- configPath,
2635
+ root: displayRoot,
2636
+ configPath: displayConfigPath,
2502
2637
  summary: {
2503
2638
  specs: specFiles.length,
2504
2639
  scenarios: scenarioFiles.length,
@@ -2507,7 +2642,7 @@ async function createReportData(root, validation, configResult) {
2507
2642
  ui: uiFiles.length,
2508
2643
  db: dbFiles.length
2509
2644
  },
2510
- counts: resolvedValidation.counts
2645
+ counts: normalizedValidation.counts
2511
2646
  },
2512
2647
  ids: {
2513
2648
  spec: idsByPrefix.SPEC,
@@ -2532,21 +2667,23 @@ async function createReportData(root, validation, configResult) {
2532
2667
  specs: {
2533
2668
  contractRefMissing: specContractRefs.missingRefSpecs.size,
2534
2669
  missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
2535
- specToContractIds: specToContractIdsRecord
2670
+ specToContracts: specToContractsRecord
2536
2671
  }
2537
2672
  },
2538
- issues: resolvedValidation.issues
2673
+ issues: normalizedValidation.issues
2539
2674
  };
2540
2675
  }
2541
2676
  function formatReportMarkdown(data) {
2542
2677
  const lines = [];
2543
2678
  lines.push("# QFAI Report");
2679
+ lines.push("");
2544
2680
  lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
2545
2681
  lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
2546
2682
  lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
2547
2683
  lines.push(`- \u7248: ${data.version}`);
2548
2684
  lines.push("");
2549
2685
  lines.push("## \u6982\u8981");
2686
+ lines.push("");
2550
2687
  lines.push(`- specs: ${data.summary.specs}`);
2551
2688
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2552
2689
  lines.push(
@@ -2557,6 +2694,7 @@ function formatReportMarkdown(data) {
2557
2694
  );
2558
2695
  lines.push("");
2559
2696
  lines.push("## ID\u96C6\u8A08");
2697
+ lines.push("");
2560
2698
  lines.push(formatIdLine("SPEC", data.ids.spec));
2561
2699
  lines.push(formatIdLine("BR", data.ids.br));
2562
2700
  lines.push(formatIdLine("SC", data.ids.sc));
@@ -2565,12 +2703,14 @@ function formatReportMarkdown(data) {
2565
2703
  lines.push(formatIdLine("DB", data.ids.db));
2566
2704
  lines.push("");
2567
2705
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
2706
+ lines.push("");
2568
2707
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
2569
2708
  lines.push(
2570
2709
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2571
2710
  );
2572
2711
  lines.push("");
2573
2712
  lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
2713
+ lines.push("");
2574
2714
  lines.push(`- total: ${data.traceability.contracts.total}`);
2575
2715
  lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
2576
2716
  lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
@@ -2579,6 +2719,7 @@ function formatReportMarkdown(data) {
2579
2719
  );
2580
2720
  lines.push("");
2581
2721
  lines.push("## \u5951\u7D04\u2192Spec");
2722
+ lines.push("");
2582
2723
  const contractToSpecs = data.traceability.contracts.idToSpecs;
2583
2724
  const contractIds = Object.keys(contractToSpecs).sort(
2584
2725
  (a, b) => a.localeCompare(b)
@@ -2597,24 +2738,25 @@ function formatReportMarkdown(data) {
2597
2738
  }
2598
2739
  lines.push("");
2599
2740
  lines.push("## Spec\u2192\u5951\u7D04");
2600
- const specToContracts = data.traceability.specs.specToContractIds;
2741
+ lines.push("");
2742
+ const specToContracts = data.traceability.specs.specToContracts;
2601
2743
  const specIds = Object.keys(specToContracts).sort(
2602
2744
  (a, b) => a.localeCompare(b)
2603
2745
  );
2604
2746
  if (specIds.length === 0) {
2605
2747
  lines.push("- (none)");
2606
2748
  } else {
2607
- for (const specId of specIds) {
2608
- const contractIds2 = specToContracts[specId] ?? [];
2609
- if (contractIds2.length === 0) {
2610
- lines.push(`- ${specId}: (none)`);
2611
- } else {
2612
- lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
2613
- }
2614
- }
2749
+ const rows = specIds.map((specId) => {
2750
+ const entry = specToContracts[specId];
2751
+ const contracts = entry?.status === "missing" ? "(missing)" : entry && entry.ids.length > 0 ? entry.ids.join(", ") : "(none)";
2752
+ const status = entry?.status ?? "missing";
2753
+ return [specId, status, contracts];
2754
+ });
2755
+ lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
2615
2756
  }
2616
2757
  lines.push("");
2617
2758
  lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
2759
+ lines.push("");
2618
2760
  const missingRefSpecs = data.traceability.specs.missingRefSpecs;
2619
2761
  if (missingRefSpecs.length === 0) {
2620
2762
  lines.push("- (none)");
@@ -2625,6 +2767,7 @@ function formatReportMarkdown(data) {
2625
2767
  }
2626
2768
  lines.push("");
2627
2769
  lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2770
+ lines.push("");
2628
2771
  lines.push(`- total: ${data.traceability.sc.total}`);
2629
2772
  lines.push(`- covered: ${data.traceability.sc.covered}`);
2630
2773
  lines.push(`- missing: ${data.traceability.sc.missing}`);
@@ -2654,6 +2797,7 @@ function formatReportMarkdown(data) {
2654
2797
  }
2655
2798
  lines.push("");
2656
2799
  lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
2800
+ lines.push("");
2657
2801
  const scRefs = data.traceability.sc.refs;
2658
2802
  const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2659
2803
  if (scIds.length === 0) {
@@ -2670,6 +2814,7 @@ function formatReportMarkdown(data) {
2670
2814
  }
2671
2815
  lines.push("");
2672
2816
  lines.push("## Spec:SC=1:1 \u9055\u53CD");
2817
+ lines.push("");
2673
2818
  const specScIssues = data.issues.filter(
2674
2819
  (item) => item.code === "QFAI-TRACE-012"
2675
2820
  );
@@ -2684,6 +2829,7 @@ function formatReportMarkdown(data) {
2684
2829
  }
2685
2830
  lines.push("");
2686
2831
  lines.push("## Hotspots");
2832
+ lines.push("");
2687
2833
  const hotspots = buildHotspots(data.issues);
2688
2834
  if (hotspots.length === 0) {
2689
2835
  lines.push("- (none)");
@@ -2696,6 +2842,7 @@ function formatReportMarkdown(data) {
2696
2842
  }
2697
2843
  lines.push("");
2698
2844
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
2845
+ lines.push("");
2699
2846
  const traceIssues = data.issues.filter(
2700
2847
  (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
2701
2848
  );
@@ -2711,6 +2858,7 @@ function formatReportMarkdown(data) {
2711
2858
  }
2712
2859
  lines.push("");
2713
2860
  lines.push("## \u691C\u8A3C\u7D50\u679C");
2861
+ lines.push("");
2714
2862
  if (data.issues.length === 0) {
2715
2863
  lines.push("- (none)");
2716
2864
  } else {
@@ -2728,7 +2876,7 @@ function formatReportJson(data) {
2728
2876
  return JSON.stringify(data, null, 2);
2729
2877
  }
2730
2878
  async function collectSpecContractRefs(specFiles, contractIdList) {
2731
- const specToContractIds = /* @__PURE__ */ new Map();
2879
+ const specToContracts = /* @__PURE__ */ new Map();
2732
2880
  const idToSpecs = /* @__PURE__ */ new Map();
2733
2881
  const missingRefSpecs = /* @__PURE__ */ new Set();
2734
2882
  for (const contractId of contractIdList) {
@@ -2737,24 +2885,31 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
2737
2885
  for (const file of specFiles) {
2738
2886
  const text = await readFile11(file, "utf-8");
2739
2887
  const parsed = parseSpec(text, file);
2740
- const specKey = parsed.specId ?? file;
2888
+ const specKey = parsed.specId;
2889
+ if (!specKey) {
2890
+ continue;
2891
+ }
2741
2892
  const refs = parsed.contractRefs;
2742
2893
  if (refs.lines.length === 0) {
2743
2894
  missingRefSpecs.add(specKey);
2895
+ specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
2744
2896
  continue;
2745
2897
  }
2746
- const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
2898
+ const current = specToContracts.get(specKey) ?? {
2899
+ status: "declared",
2900
+ ids: /* @__PURE__ */ new Set()
2901
+ };
2747
2902
  for (const id of refs.ids) {
2748
- currentContracts.add(id);
2903
+ current.ids.add(id);
2749
2904
  const specs = idToSpecs.get(id);
2750
2905
  if (specs) {
2751
2906
  specs.add(specKey);
2752
2907
  }
2753
2908
  }
2754
- specToContractIds.set(specKey, currentContracts);
2909
+ specToContracts.set(specKey, current);
2755
2910
  }
2756
2911
  return {
2757
- specToContractIds,
2912
+ specToContracts,
2758
2913
  idToSpecs,
2759
2914
  missingRefSpecs
2760
2915
  };
@@ -2831,6 +2986,20 @@ function formatList(values) {
2831
2986
  }
2832
2987
  return values.join(", ");
2833
2988
  }
2989
+ function formatMarkdownTable(headers, rows) {
2990
+ const widths = headers.map((header, index) => {
2991
+ const candidates = rows.map((row) => row[index] ?? "");
2992
+ return Math.max(header.length, ...candidates.map((item) => item.length));
2993
+ });
2994
+ const formatRow = (cells) => {
2995
+ const padded = cells.map(
2996
+ (cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)
2997
+ );
2998
+ return `| ${padded.join(" | ")} |`;
2999
+ };
3000
+ const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
3001
+ return [formatRow(headers), separator, ...rows.map(formatRow)];
3002
+ }
2834
3003
  function toSortedArray2(values) {
2835
3004
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2836
3005
  }
@@ -2841,6 +3010,27 @@ function mapToSortedRecord(values) {
2841
3010
  }
2842
3011
  return record2;
2843
3012
  }
3013
+ function mapToSpecContractRecord(values) {
3014
+ const record2 = {};
3015
+ for (const [key, entry] of values.entries()) {
3016
+ record2[key] = {
3017
+ status: entry.status,
3018
+ ids: toSortedArray2(entry.ids)
3019
+ };
3020
+ }
3021
+ return record2;
3022
+ }
3023
+ function normalizeScSources(root, sources) {
3024
+ const normalized = /* @__PURE__ */ new Map();
3025
+ for (const [id, files] of sources.entries()) {
3026
+ const mapped = /* @__PURE__ */ new Set();
3027
+ for (const file of files) {
3028
+ mapped.add(toRelativePath(root, file));
3029
+ }
3030
+ normalized.set(id, mapped);
3031
+ }
3032
+ return normalized;
3033
+ }
2844
3034
  function buildHotspots(issues) {
2845
3035
  const map = /* @__PURE__ */ new Map();
2846
3036
  for (const issue7 of issues) {
@@ -2868,6 +3058,7 @@ export {
2868
3058
  extractAllIds,
2869
3059
  extractIds,
2870
3060
  extractInvalidIds,
3061
+ findConfigRoot,
2871
3062
  formatReportJson,
2872
3063
  formatReportMarkdown,
2873
3064
  getConfigPath,