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.
@@ -139,6 +139,10 @@ function info(message) {
139
139
  process.stdout.write(`${message}
140
140
  `);
141
141
  }
142
+ function warn(message) {
143
+ process.stdout.write(`${message}
144
+ `);
145
+ }
142
146
  function error(message) {
143
147
  process.stderr.write(`${message}
144
148
  `);
@@ -178,7 +182,7 @@ function report(copied, skipped, dryRun, label) {
178
182
 
179
183
  // src/cli/commands/report.ts
180
184
  var import_promises16 = require("fs/promises");
181
- var import_node_path15 = __toESM(require("path"), 1);
185
+ var import_node_path16 = __toESM(require("path"), 1);
182
186
 
183
187
  // src/core/config.ts
184
188
  var import_promises2 = require("fs/promises");
@@ -213,7 +217,7 @@ var defaultConfig = {
213
217
  testFileGlobs: [],
214
218
  testFileExcludeGlobs: [],
215
219
  scNoTestSeverity: "error",
216
- allowOrphanContracts: false,
220
+ orphanContractsPolicy: "error",
217
221
  unknownContractIdSeverity: "error"
218
222
  }
219
223
  },
@@ -224,6 +228,26 @@ var defaultConfig = {
224
228
  function getConfigPath(root) {
225
229
  return import_node_path4.default.join(root, "qfai.config.yaml");
226
230
  }
231
+ async function findConfigRoot(startDir) {
232
+ const resolvedStart = import_node_path4.default.resolve(startDir);
233
+ let current = resolvedStart;
234
+ while (true) {
235
+ const configPath = getConfigPath(current);
236
+ if (await exists2(configPath)) {
237
+ return { root: current, configPath, found: true };
238
+ }
239
+ const parent = import_node_path4.default.dirname(current);
240
+ if (parent === current) {
241
+ break;
242
+ }
243
+ current = parent;
244
+ }
245
+ return {
246
+ root: resolvedStart,
247
+ configPath: getConfigPath(resolvedStart),
248
+ found: false
249
+ };
250
+ }
227
251
  async function loadConfig(root) {
228
252
  const configPath = getConfigPath(root);
229
253
  const issues = [];
@@ -413,10 +437,10 @@ function normalizeValidation(raw, configPath, issues) {
413
437
  configPath,
414
438
  issues
415
439
  ),
416
- allowOrphanContracts: readBoolean(
417
- traceabilityRaw?.allowOrphanContracts,
418
- base.traceability.allowOrphanContracts,
419
- "validation.traceability.allowOrphanContracts",
440
+ orphanContractsPolicy: readOrphanContractsPolicy(
441
+ traceabilityRaw?.orphanContractsPolicy,
442
+ base.traceability.orphanContractsPolicy,
443
+ "validation.traceability.orphanContractsPolicy",
420
444
  configPath,
421
445
  issues
422
446
  ),
@@ -512,6 +536,20 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
512
536
  }
513
537
  return fallback;
514
538
  }
539
+ function readOrphanContractsPolicy(value, fallback, label, configPath, issues) {
540
+ if (value === "error" || value === "warning" || value === "allow") {
541
+ return value;
542
+ }
543
+ if (value !== void 0) {
544
+ issues.push(
545
+ configIssue(
546
+ configPath,
547
+ `${label} \u306F error|warning|allow \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
548
+ )
549
+ );
550
+ }
551
+ return fallback;
552
+ }
515
553
  function configIssue(file, message) {
516
554
  return {
517
555
  code: "QFAI_CONFIG_INVALID",
@@ -527,6 +565,14 @@ function isMissingFile(error2) {
527
565
  }
528
566
  return false;
529
567
  }
568
+ async function exists2(target) {
569
+ try {
570
+ await (0, import_promises2.access)(target);
571
+ return true;
572
+ } catch {
573
+ return false;
574
+ }
575
+ }
530
576
  function formatError(error2) {
531
577
  if (error2 instanceof Error) {
532
578
  return error2.message;
@@ -537,20 +583,76 @@ function isRecord(value) {
537
583
  return value !== null && typeof value === "object" && !Array.isArray(value);
538
584
  }
539
585
 
586
+ // src/core/paths.ts
587
+ var import_node_path5 = __toESM(require("path"), 1);
588
+ function toRelativePath(root, target) {
589
+ if (!target) {
590
+ return target;
591
+ }
592
+ if (!import_node_path5.default.isAbsolute(target)) {
593
+ return toPosixPath(target);
594
+ }
595
+ const relative = import_node_path5.default.relative(root, target);
596
+ if (!relative) {
597
+ return ".";
598
+ }
599
+ return toPosixPath(relative);
600
+ }
601
+ function toPosixPath(value) {
602
+ return value.replace(/\\/g, "/");
603
+ }
604
+
605
+ // src/core/normalize.ts
606
+ function normalizeIssuePaths(root, issues) {
607
+ return issues.map((issue7) => {
608
+ if (!issue7.file) {
609
+ return issue7;
610
+ }
611
+ const normalized = toRelativePath(root, issue7.file);
612
+ if (normalized === issue7.file) {
613
+ return issue7;
614
+ }
615
+ return {
616
+ ...issue7,
617
+ file: normalized
618
+ };
619
+ });
620
+ }
621
+ function normalizeScCoverage(root, sc) {
622
+ const refs = {};
623
+ for (const [scId, files] of Object.entries(sc.refs)) {
624
+ refs[scId] = files.map((file) => toRelativePath(root, file));
625
+ }
626
+ return {
627
+ ...sc,
628
+ refs
629
+ };
630
+ }
631
+ function normalizeValidationResult(root, result) {
632
+ return {
633
+ ...result,
634
+ issues: normalizeIssuePaths(root, result.issues),
635
+ traceability: {
636
+ ...result.traceability,
637
+ sc: normalizeScCoverage(root, result.traceability.sc)
638
+ }
639
+ };
640
+ }
641
+
540
642
  // src/core/report.ts
541
643
  var import_promises15 = require("fs/promises");
542
- var import_node_path14 = __toESM(require("path"), 1);
644
+ var import_node_path15 = __toESM(require("path"), 1);
543
645
 
544
646
  // src/core/contractIndex.ts
545
647
  var import_promises6 = require("fs/promises");
546
- var import_node_path7 = __toESM(require("path"), 1);
648
+ var import_node_path8 = __toESM(require("path"), 1);
547
649
 
548
650
  // src/core/discovery.ts
549
651
  var import_promises5 = require("fs/promises");
550
652
 
551
653
  // src/core/fs.ts
552
654
  var import_promises3 = require("fs/promises");
553
- var import_node_path5 = __toESM(require("path"), 1);
655
+ var import_node_path6 = __toESM(require("path"), 1);
554
656
  var import_fast_glob = __toESM(require("fast-glob"), 1);
555
657
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
556
658
  "node_modules",
@@ -562,7 +664,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
562
664
  ]);
563
665
  async function collectFiles(root, options = {}) {
564
666
  const entries = [];
565
- if (!await exists2(root)) {
667
+ if (!await exists3(root)) {
566
668
  return entries;
567
669
  }
568
670
  const ignoreDirs = /* @__PURE__ */ new Set([
@@ -588,7 +690,7 @@ async function collectFilesByGlobs(root, options) {
588
690
  async function walk(base, current, ignoreDirs, extensions, out) {
589
691
  const items = await (0, import_promises3.readdir)(current, { withFileTypes: true });
590
692
  for (const item of items) {
591
- const fullPath = import_node_path5.default.join(current, item.name);
693
+ const fullPath = import_node_path6.default.join(current, item.name);
592
694
  if (item.isDirectory()) {
593
695
  if (ignoreDirs.has(item.name)) {
594
696
  continue;
@@ -598,7 +700,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
598
700
  }
599
701
  if (item.isFile()) {
600
702
  if (extensions.length > 0) {
601
- const ext = import_node_path5.default.extname(item.name).toLowerCase();
703
+ const ext = import_node_path6.default.extname(item.name).toLowerCase();
602
704
  if (!extensions.includes(ext)) {
603
705
  continue;
604
706
  }
@@ -607,7 +709,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
607
709
  }
608
710
  }
609
711
  }
610
- async function exists2(target) {
712
+ async function exists3(target) {
611
713
  try {
612
714
  await (0, import_promises3.access)(target);
613
715
  return true;
@@ -618,22 +720,22 @@ async function exists2(target) {
618
720
 
619
721
  // src/core/specLayout.ts
620
722
  var import_promises4 = require("fs/promises");
621
- var import_node_path6 = __toESM(require("path"), 1);
723
+ var import_node_path7 = __toESM(require("path"), 1);
622
724
  var SPEC_DIR_RE = /^spec-\d{4}$/;
623
725
  async function collectSpecEntries(specsRoot) {
624
726
  const dirs = await listSpecDirs(specsRoot);
625
727
  const entries = dirs.map((dir) => ({
626
728
  dir,
627
- specPath: import_node_path6.default.join(dir, "spec.md"),
628
- deltaPath: import_node_path6.default.join(dir, "delta.md"),
629
- scenarioPath: import_node_path6.default.join(dir, "scenario.md")
729
+ specPath: import_node_path7.default.join(dir, "spec.md"),
730
+ deltaPath: import_node_path7.default.join(dir, "delta.md"),
731
+ scenarioPath: import_node_path7.default.join(dir, "scenario.md")
630
732
  }));
631
733
  return entries.sort((a, b) => a.dir.localeCompare(b.dir));
632
734
  }
633
735
  async function listSpecDirs(specsRoot) {
634
736
  try {
635
737
  const items = await (0, import_promises4.readdir)(specsRoot, { withFileTypes: true });
636
- return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path6.default.join(specsRoot, name));
738
+ return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path7.default.join(specsRoot, name));
637
739
  } catch (error2) {
638
740
  if (isMissingFileError(error2)) {
639
741
  return [];
@@ -681,13 +783,13 @@ async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
681
783
  async function filterExisting(files) {
682
784
  const existing = [];
683
785
  for (const file of files) {
684
- if (await exists3(file)) {
786
+ if (await exists4(file)) {
685
787
  existing.push(file);
686
788
  }
687
789
  }
688
790
  return existing;
689
791
  }
690
- async function exists3(target) {
792
+ async function exists4(target) {
691
793
  try {
692
794
  await (0, import_promises5.access)(target);
693
795
  return true;
@@ -716,9 +818,9 @@ function stripContractDeclarationLines(text) {
716
818
  // src/core/contractIndex.ts
717
819
  async function buildContractIndex(root, config) {
718
820
  const contractsRoot = resolvePath(root, config, "contractsDir");
719
- const uiRoot = import_node_path7.default.join(contractsRoot, "ui");
720
- const apiRoot = import_node_path7.default.join(contractsRoot, "api");
721
- const dbRoot = import_node_path7.default.join(contractsRoot, "db");
821
+ const uiRoot = import_node_path8.default.join(contractsRoot, "ui");
822
+ const apiRoot = import_node_path8.default.join(contractsRoot, "api");
823
+ const dbRoot = import_node_path8.default.join(contractsRoot, "db");
722
824
  const [uiFiles, apiFiles, dbFiles] = await Promise.all([
723
825
  collectUiContractFiles(uiRoot),
724
826
  collectApiContractFiles(apiRoot),
@@ -800,6 +902,57 @@ function isValidId(value, prefix) {
800
902
  return strict.test(value);
801
903
  }
802
904
 
905
+ // src/core/parse/contractRefs.ts
906
+ var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
907
+ function parseContractRefs(text, options = {}) {
908
+ const linePattern = buildLinePattern(options);
909
+ const lines = [];
910
+ for (const match of text.matchAll(linePattern)) {
911
+ lines.push((match[1] ?? "").trim());
912
+ }
913
+ const ids = [];
914
+ const invalidTokens = [];
915
+ let hasNone = false;
916
+ for (const line of lines) {
917
+ if (line.length === 0) {
918
+ invalidTokens.push("(empty)");
919
+ continue;
920
+ }
921
+ const tokens = line.split(",").map((token) => token.trim());
922
+ for (const token of tokens) {
923
+ if (token.length === 0) {
924
+ invalidTokens.push("(empty)");
925
+ continue;
926
+ }
927
+ if (token === "none") {
928
+ hasNone = true;
929
+ continue;
930
+ }
931
+ if (CONTRACT_REF_ID_RE.test(token)) {
932
+ ids.push(token);
933
+ continue;
934
+ }
935
+ invalidTokens.push(token);
936
+ }
937
+ }
938
+ return {
939
+ lines,
940
+ ids: unique2(ids),
941
+ invalidTokens: unique2(invalidTokens),
942
+ hasNone
943
+ };
944
+ }
945
+ function buildLinePattern(options) {
946
+ const prefix = options.allowCommentPrefix ? "#" : "";
947
+ return new RegExp(
948
+ `^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
949
+ "gm"
950
+ );
951
+ }
952
+ function unique2(values) {
953
+ return Array.from(new Set(values));
954
+ }
955
+
803
956
  // src/core/parse/markdown.ts
804
957
  var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
805
958
  function parseHeadings(md) {
@@ -846,8 +999,6 @@ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
846
999
  var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
847
1000
  var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
848
1001
  var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
849
- var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
850
- var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
851
1002
  var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
852
1003
  var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
853
1004
  function parseSpec(md, file) {
@@ -920,50 +1071,10 @@ function parseSpec(md, file) {
920
1071
  }
921
1072
  return parsed;
922
1073
  }
923
- function parseContractRefs(md) {
924
- const lines = [];
925
- for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
926
- lines.push((match[1] ?? "").trim());
927
- }
928
- const ids = [];
929
- const invalidTokens = [];
930
- let hasNone = false;
931
- for (const line of lines) {
932
- if (line.length === 0) {
933
- invalidTokens.push("(empty)");
934
- continue;
935
- }
936
- const tokens = line.split(",").map((token) => token.trim());
937
- for (const token of tokens) {
938
- if (token.length === 0) {
939
- invalidTokens.push("(empty)");
940
- continue;
941
- }
942
- if (token === "none") {
943
- hasNone = true;
944
- continue;
945
- }
946
- if (CONTRACT_REF_ID_RE.test(token)) {
947
- ids.push(token);
948
- continue;
949
- }
950
- invalidTokens.push(token);
951
- }
952
- }
953
- return {
954
- lines,
955
- ids: unique2(ids),
956
- invalidTokens: unique2(invalidTokens),
957
- hasNone
958
- };
959
- }
960
- function unique2(values) {
961
- return Array.from(new Set(values));
962
- }
963
1074
 
964
1075
  // src/core/traceability.ts
965
1076
  var import_promises7 = require("fs/promises");
966
- var import_node_path8 = __toESM(require("path"), 1);
1077
+ var import_node_path9 = __toESM(require("path"), 1);
967
1078
 
968
1079
  // src/core/gherkin/parse.ts
969
1080
  var import_gherkin = require("@cucumber/gherkin");
@@ -994,9 +1105,6 @@ function formatError2(error2) {
994
1105
  var SPEC_TAG_RE = /^SPEC-\d{4}$/;
995
1106
  var SC_TAG_RE = /^SC-\d{4}$/;
996
1107
  var BR_TAG_RE = /^BR-\d{4}$/;
997
- var UI_TAG_RE = /^UI-\d{4}$/;
998
- var API_TAG_RE = /^API-\d{4}$/;
999
- var DB_TAG_RE = /^DB-\d{4}$/;
1000
1108
  function parseScenarioDocument(text, uri) {
1001
1109
  const { gherkinDocument, errors } = parseGherkin(text, uri);
1002
1110
  if (!gherkinDocument) {
@@ -1021,31 +1129,21 @@ function parseScenarioDocument(text, uri) {
1021
1129
  errors
1022
1130
  };
1023
1131
  }
1024
- function buildScenarioAtoms(document) {
1132
+ function buildScenarioAtoms(document, contractIds = []) {
1133
+ const uniqueContractIds = unique3(contractIds).sort(
1134
+ (a, b) => a.localeCompare(b)
1135
+ );
1025
1136
  return document.scenarios.map((scenario) => {
1026
1137
  const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1027
1138
  const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1028
1139
  const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1029
- const contractIds = /* @__PURE__ */ new Set();
1030
- scenario.tags.forEach((tag) => {
1031
- if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
1032
- contractIds.add(tag);
1033
- }
1034
- });
1035
- for (const step of scenario.steps) {
1036
- for (const text of collectStepTexts(step)) {
1037
- extractIds(text, "UI").forEach((id) => contractIds.add(id));
1038
- extractIds(text, "API").forEach((id) => contractIds.add(id));
1039
- extractIds(text, "DB").forEach((id) => contractIds.add(id));
1040
- }
1041
- }
1042
1140
  const atom = {
1043
1141
  uri: document.uri,
1044
1142
  featureName: document.featureName ?? "",
1045
1143
  scenarioName: scenario.name,
1046
1144
  kind: scenario.kind,
1047
1145
  brIds,
1048
- contractIds: Array.from(contractIds).sort()
1146
+ contractIds: uniqueContractIds
1049
1147
  };
1050
1148
  if (scenario.line !== void 0) {
1051
1149
  atom.line = scenario.line;
@@ -1098,23 +1196,6 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
1098
1196
  function collectTagNames(tags) {
1099
1197
  return tags.map((tag) => tag.name.replace(/^@/, ""));
1100
1198
  }
1101
- function collectStepTexts(step) {
1102
- const texts = [];
1103
- if (step.text) {
1104
- texts.push(step.text);
1105
- }
1106
- if (step.docString?.content) {
1107
- texts.push(step.docString.content);
1108
- }
1109
- if (step.dataTable?.rows) {
1110
- for (const row of step.dataTable.rows) {
1111
- for (const cell of row.cells) {
1112
- texts.push(cell.value);
1113
- }
1114
- }
1115
- }
1116
- return texts;
1117
- }
1118
1199
  function unique3(values) {
1119
1200
  return Array.from(new Set(values));
1120
1201
  }
@@ -1216,7 +1297,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1216
1297
  };
1217
1298
  }
1218
1299
  const normalizedFiles = Array.from(
1219
- new Set(files.map((file) => import_node_path8.default.normalize(file)))
1300
+ new Set(files.map((file) => import_node_path9.default.normalize(file)))
1220
1301
  );
1221
1302
  for (const file of normalizedFiles) {
1222
1303
  const text = await (0, import_promises7.readFile)(file, "utf-8");
@@ -1277,11 +1358,11 @@ function formatError3(error2) {
1277
1358
 
1278
1359
  // src/core/version.ts
1279
1360
  var import_promises8 = require("fs/promises");
1280
- var import_node_path9 = __toESM(require("path"), 1);
1361
+ var import_node_path10 = __toESM(require("path"), 1);
1281
1362
  var import_node_url2 = require("url");
1282
1363
  async function resolveToolVersion() {
1283
- if ("0.4.9".length > 0) {
1284
- return "0.4.9";
1364
+ if ("0.5.2".length > 0) {
1365
+ return "0.5.2";
1285
1366
  }
1286
1367
  try {
1287
1368
  const packagePath = resolvePackageJsonPath();
@@ -1296,18 +1377,18 @@ async function resolveToolVersion() {
1296
1377
  function resolvePackageJsonPath() {
1297
1378
  const base = __filename;
1298
1379
  const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
1299
- return import_node_path9.default.resolve(import_node_path9.default.dirname(basePath), "../../package.json");
1380
+ return import_node_path10.default.resolve(import_node_path10.default.dirname(basePath), "../../package.json");
1300
1381
  }
1301
1382
 
1302
1383
  // src/core/validators/contracts.ts
1303
1384
  var import_promises9 = require("fs/promises");
1304
- var import_node_path11 = __toESM(require("path"), 1);
1385
+ var import_node_path12 = __toESM(require("path"), 1);
1305
1386
 
1306
1387
  // src/core/contracts.ts
1307
- var import_node_path10 = __toESM(require("path"), 1);
1388
+ var import_node_path11 = __toESM(require("path"), 1);
1308
1389
  var import_yaml2 = require("yaml");
1309
1390
  function parseStructuredContract(file, text) {
1310
- const ext = import_node_path10.default.extname(file).toLowerCase();
1391
+ const ext = import_node_path11.default.extname(file).toLowerCase();
1311
1392
  if (ext === ".json") {
1312
1393
  return JSON.parse(text);
1313
1394
  }
@@ -1327,9 +1408,9 @@ var SQL_DANGEROUS_PATTERNS = [
1327
1408
  async function validateContracts(root, config) {
1328
1409
  const issues = [];
1329
1410
  const contractsRoot = resolvePath(root, config, "contractsDir");
1330
- issues.push(...await validateUiContracts(import_node_path11.default.join(contractsRoot, "ui")));
1331
- issues.push(...await validateApiContracts(import_node_path11.default.join(contractsRoot, "api")));
1332
- issues.push(...await validateDbContracts(import_node_path11.default.join(contractsRoot, "db")));
1411
+ issues.push(...await validateUiContracts(import_node_path12.default.join(contractsRoot, "ui")));
1412
+ issues.push(...await validateApiContracts(import_node_path12.default.join(contractsRoot, "api")));
1413
+ issues.push(...await validateDbContracts(import_node_path12.default.join(contractsRoot, "db")));
1333
1414
  const contractIndex = await buildContractIndex(root, config);
1334
1415
  issues.push(...validateDuplicateContractIds(contractIndex));
1335
1416
  return issues;
@@ -1612,7 +1693,7 @@ function issue(code, message, severity, file, rule, refs) {
1612
1693
 
1613
1694
  // src/core/validators/delta.ts
1614
1695
  var import_promises10 = require("fs/promises");
1615
- var import_node_path12 = __toESM(require("path"), 1);
1696
+ var import_node_path13 = __toESM(require("path"), 1);
1616
1697
  var SECTION_RE = /^##\s+変更区分/m;
1617
1698
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1618
1699
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1626,7 +1707,7 @@ async function validateDeltas(root, config) {
1626
1707
  }
1627
1708
  const issues = [];
1628
1709
  for (const pack of packs) {
1629
- const deltaPath = import_node_path12.default.join(pack, "delta.md");
1710
+ const deltaPath = import_node_path13.default.join(pack, "delta.md");
1630
1711
  let text;
1631
1712
  try {
1632
1713
  text = await (0, import_promises10.readFile)(deltaPath, "utf-8");
@@ -1702,7 +1783,7 @@ function issue2(code, message, severity, file, rule, refs) {
1702
1783
 
1703
1784
  // src/core/validators/ids.ts
1704
1785
  var import_promises11 = require("fs/promises");
1705
- var import_node_path13 = __toESM(require("path"), 1);
1786
+ var import_node_path14 = __toESM(require("path"), 1);
1706
1787
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1707
1788
  async function validateDefinedIds(root, config) {
1708
1789
  const issues = [];
@@ -1768,7 +1849,7 @@ function recordId(out, id, file) {
1768
1849
  }
1769
1850
  function formatFileList(files, root) {
1770
1851
  return files.map((file) => {
1771
- const relative = import_node_path13.default.relative(root, file);
1852
+ const relative = import_node_path14.default.relative(root, file);
1772
1853
  return relative.length > 0 ? relative : file;
1773
1854
  }).join(", ");
1774
1855
  }
@@ -2205,7 +2286,7 @@ async function validateTraceability(root, config) {
2205
2286
  if (contractRefs.hasNone && contractRefs.ids.length > 0) {
2206
2287
  issues.push(
2207
2288
  issue6(
2208
- "QFAI-TRACE-021",
2289
+ "QFAI-TRACE-023",
2209
2290
  "Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2210
2291
  "error",
2211
2292
  file,
@@ -2237,7 +2318,7 @@ async function validateTraceability(root, config) {
2237
2318
  if (unknownContractIds.length > 0) {
2238
2319
  issues.push(
2239
2320
  issue6(
2240
- "QFAI-TRACE-021",
2321
+ "QFAI-TRACE-024",
2241
2322
  `Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
2242
2323
  ", "
2243
2324
  )}`,
@@ -2252,11 +2333,62 @@ async function validateTraceability(root, config) {
2252
2333
  for (const file of scenarioFiles) {
2253
2334
  const text = await (0, import_promises14.readFile)(file, "utf-8");
2254
2335
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2336
+ const scenarioContractRefs = parseContractRefs(text, {
2337
+ allowCommentPrefix: true
2338
+ });
2339
+ if (scenarioContractRefs.lines.length === 0) {
2340
+ issues.push(
2341
+ issue6(
2342
+ "QFAI-TRACE-031",
2343
+ "Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2344
+ "error",
2345
+ file,
2346
+ "traceability.scenarioContractRefRequired"
2347
+ )
2348
+ );
2349
+ } else {
2350
+ if (scenarioContractRefs.hasNone && scenarioContractRefs.ids.length > 0) {
2351
+ issues.push(
2352
+ issue6(
2353
+ "QFAI-TRACE-033",
2354
+ "Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2355
+ "error",
2356
+ file,
2357
+ "traceability.scenarioContractRefFormat"
2358
+ )
2359
+ );
2360
+ }
2361
+ if (scenarioContractRefs.invalidTokens.length > 0) {
2362
+ issues.push(
2363
+ issue6(
2364
+ "QFAI-TRACE-032",
2365
+ `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
2366
+ ", "
2367
+ )}`,
2368
+ "error",
2369
+ file,
2370
+ "traceability.scenarioContractRefFormat",
2371
+ scenarioContractRefs.invalidTokens
2372
+ )
2373
+ );
2374
+ }
2375
+ }
2255
2376
  const { document, errors } = parseScenarioDocument(text, file);
2256
2377
  if (!document || errors.length > 0) {
2257
2378
  continue;
2258
2379
  }
2259
- const atoms = buildScenarioAtoms(document);
2380
+ if (document.scenarios.length !== 1) {
2381
+ issues.push(
2382
+ issue6(
2383
+ "QFAI-TRACE-030",
2384
+ `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})`,
2385
+ "error",
2386
+ file,
2387
+ "traceability.scenarioOnePerFile"
2388
+ )
2389
+ );
2390
+ }
2391
+ const atoms = buildScenarioAtoms(document, scenarioContractRefs.ids);
2260
2392
  const scIdsInFile = /* @__PURE__ */ new Set();
2261
2393
  for (const [index, scenario] of document.scenarios.entries()) {
2262
2394
  const atom = atoms[index];
@@ -2401,7 +2533,7 @@ async function validateTraceability(root, config) {
2401
2533
  if (orphanBrIds.length > 0) {
2402
2534
  issues.push(
2403
2535
  issue6(
2404
- "QFAI_TRACE_BR_ORPHAN",
2536
+ "QFAI-TRACE-009",
2405
2537
  `BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
2406
2538
  "error",
2407
2539
  specsRoot,
@@ -2471,17 +2603,19 @@ async function validateTraceability(root, config) {
2471
2603
  );
2472
2604
  }
2473
2605
  }
2474
- if (!config.validation.traceability.allowOrphanContracts) {
2606
+ const orphanPolicy = config.validation.traceability.orphanContractsPolicy;
2607
+ if (orphanPolicy !== "allow") {
2475
2608
  if (contractIds.size > 0) {
2476
2609
  const orphanContracts = Array.from(contractIds).filter(
2477
2610
  (id) => !specContractIds.has(id)
2478
2611
  );
2479
2612
  if (orphanContracts.length > 0) {
2613
+ const severity = orphanPolicy === "warning" ? "warning" : "error";
2480
2614
  issues.push(
2481
2615
  issue6(
2482
2616
  "QFAI-TRACE-022",
2483
2617
  `\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
2484
- "error",
2618
+ severity,
2485
2619
  specsRoot,
2486
2620
  "traceability.contractCoverage",
2487
2621
  orphanContracts
@@ -2606,16 +2740,17 @@ function countIssues(issues) {
2606
2740
  // src/core/report.ts
2607
2741
  var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2608
2742
  async function createReportData(root, validation, configResult) {
2609
- const resolved = configResult ?? await loadConfig(root);
2743
+ const resolvedRoot = import_node_path15.default.resolve(root);
2744
+ const resolved = configResult ?? await loadConfig(resolvedRoot);
2610
2745
  const config = resolved.config;
2611
2746
  const configPath = resolved.configPath;
2612
- const specsRoot = resolvePath(root, config, "specsDir");
2613
- const contractsRoot = resolvePath(root, config, "contractsDir");
2614
- const apiRoot = import_node_path14.default.join(contractsRoot, "api");
2615
- const uiRoot = import_node_path14.default.join(contractsRoot, "ui");
2616
- const dbRoot = import_node_path14.default.join(contractsRoot, "db");
2617
- const srcRoot = resolvePath(root, config, "srcDir");
2618
- const testsRoot = resolvePath(root, config, "testsDir");
2747
+ const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
2748
+ const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
2749
+ const apiRoot = import_node_path15.default.join(contractsRoot, "api");
2750
+ const uiRoot = import_node_path15.default.join(contractsRoot, "ui");
2751
+ const dbRoot = import_node_path15.default.join(contractsRoot, "db");
2752
+ const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
2753
+ const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
2619
2754
  const specFiles = await collectSpecFiles(specsRoot);
2620
2755
  const scenarioFiles = await collectScenarioFiles(specsRoot);
2621
2756
  const {
@@ -2623,12 +2758,15 @@ async function createReportData(root, validation, configResult) {
2623
2758
  ui: uiFiles,
2624
2759
  db: dbFiles
2625
2760
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
2626
- const contractIndex = await buildContractIndex(root, config);
2627
- const specContractRefs = await collectSpecContractRefs(specFiles);
2761
+ const contractIndex = await buildContractIndex(resolvedRoot, config);
2628
2762
  const contractIdList = Array.from(contractIndex.ids);
2763
+ const specContractRefs = await collectSpecContractRefs(
2764
+ specFiles,
2765
+ contractIdList
2766
+ );
2629
2767
  const referencedContracts = /* @__PURE__ */ new Set();
2630
- for (const ids of specContractRefs.specToContractIds.values()) {
2631
- ids.forEach((id) => referencedContracts.add(id));
2768
+ for (const entry of specContractRefs.specToContracts.values()) {
2769
+ entry.ids.forEach((id) => referencedContracts.add(id));
2632
2770
  }
2633
2771
  const referencedContractCount = contractIdList.filter(
2634
2772
  (id) => referencedContracts.has(id)
@@ -2637,8 +2775,8 @@ async function createReportData(root, validation, configResult) {
2637
2775
  (id) => !referencedContracts.has(id)
2638
2776
  ).length;
2639
2777
  const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
2640
- const specToContractIdsRecord = mapToSortedRecord(
2641
- specContractRefs.specToContractIds
2778
+ const specToContractsRecord = mapToSpecContractRecord(
2779
+ specContractRefs.specToContracts
2642
2780
  );
2643
2781
  const idsByPrefix = await collectIds([
2644
2782
  ...specFiles,
@@ -2656,24 +2794,26 @@ async function createReportData(root, validation, configResult) {
2656
2794
  srcRoot,
2657
2795
  testsRoot
2658
2796
  );
2659
- const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
2660
- const scRefsResult = await collectScTestReferences(
2661
- root,
2662
- config.validation.traceability.testFileGlobs,
2663
- config.validation.traceability.testFileExcludeGlobs
2797
+ const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
2798
+ const normalizedValidation = normalizeValidationResult(
2799
+ resolvedRoot,
2800
+ resolvedValidationRaw
2664
2801
  );
2665
- const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
2666
- const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
2802
+ const scCoverage = normalizedValidation.traceability.sc;
2803
+ const testFiles = normalizedValidation.traceability.testFiles;
2667
2804
  const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
2668
- const scSourceRecord = mapToSortedRecord(scSources);
2669
- const resolvedValidation = validation ?? await validateProject(root, resolved);
2805
+ const scSourceRecord = mapToSortedRecord(
2806
+ normalizeScSources(resolvedRoot, scSources)
2807
+ );
2670
2808
  const version = await resolveToolVersion();
2809
+ const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
2810
+ const displayConfigPath = toRelativePath(resolvedRoot, configPath);
2671
2811
  return {
2672
2812
  tool: "qfai",
2673
2813
  version,
2674
2814
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2675
- root,
2676
- configPath,
2815
+ root: displayRoot,
2816
+ configPath: displayConfigPath,
2677
2817
  summary: {
2678
2818
  specs: specFiles.length,
2679
2819
  scenarios: scenarioFiles.length,
@@ -2682,7 +2822,7 @@ async function createReportData(root, validation, configResult) {
2682
2822
  ui: uiFiles.length,
2683
2823
  db: dbFiles.length
2684
2824
  },
2685
- counts: resolvedValidation.counts
2825
+ counts: normalizedValidation.counts
2686
2826
  },
2687
2827
  ids: {
2688
2828
  spec: idsByPrefix.SPEC,
@@ -2706,21 +2846,24 @@ async function createReportData(root, validation, configResult) {
2706
2846
  },
2707
2847
  specs: {
2708
2848
  contractRefMissing: specContractRefs.missingRefSpecs.size,
2709
- specToContractIds: specToContractIdsRecord
2849
+ missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
2850
+ specToContracts: specToContractsRecord
2710
2851
  }
2711
2852
  },
2712
- issues: resolvedValidation.issues
2853
+ issues: normalizedValidation.issues
2713
2854
  };
2714
2855
  }
2715
2856
  function formatReportMarkdown(data) {
2716
2857
  const lines = [];
2717
2858
  lines.push("# QFAI Report");
2859
+ lines.push("");
2718
2860
  lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
2719
2861
  lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
2720
2862
  lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
2721
2863
  lines.push(`- \u7248: ${data.version}`);
2722
2864
  lines.push("");
2723
2865
  lines.push("## \u6982\u8981");
2866
+ lines.push("");
2724
2867
  lines.push(`- specs: ${data.summary.specs}`);
2725
2868
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2726
2869
  lines.push(
@@ -2731,6 +2874,7 @@ function formatReportMarkdown(data) {
2731
2874
  );
2732
2875
  lines.push("");
2733
2876
  lines.push("## ID\u96C6\u8A08");
2877
+ lines.push("");
2734
2878
  lines.push(formatIdLine("SPEC", data.ids.spec));
2735
2879
  lines.push(formatIdLine("BR", data.ids.br));
2736
2880
  lines.push(formatIdLine("SC", data.ids.sc));
@@ -2739,12 +2883,14 @@ function formatReportMarkdown(data) {
2739
2883
  lines.push(formatIdLine("DB", data.ids.db));
2740
2884
  lines.push("");
2741
2885
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
2886
+ lines.push("");
2742
2887
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
2743
2888
  lines.push(
2744
2889
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2745
2890
  );
2746
2891
  lines.push("");
2747
2892
  lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
2893
+ lines.push("");
2748
2894
  lines.push(`- total: ${data.traceability.contracts.total}`);
2749
2895
  lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
2750
2896
  lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
@@ -2753,6 +2899,7 @@ function formatReportMarkdown(data) {
2753
2899
  );
2754
2900
  lines.push("");
2755
2901
  lines.push("## \u5951\u7D04\u2192Spec");
2902
+ lines.push("");
2756
2903
  const contractToSpecs = data.traceability.contracts.idToSpecs;
2757
2904
  const contractIds = Object.keys(contractToSpecs).sort(
2758
2905
  (a, b) => a.localeCompare(b)
@@ -2771,24 +2918,36 @@ function formatReportMarkdown(data) {
2771
2918
  }
2772
2919
  lines.push("");
2773
2920
  lines.push("## Spec\u2192\u5951\u7D04");
2774
- const specToContracts = data.traceability.specs.specToContractIds;
2921
+ lines.push("");
2922
+ const specToContracts = data.traceability.specs.specToContracts;
2775
2923
  const specIds = Object.keys(specToContracts).sort(
2776
2924
  (a, b) => a.localeCompare(b)
2777
2925
  );
2778
2926
  if (specIds.length === 0) {
2779
2927
  lines.push("- (none)");
2780
2928
  } else {
2781
- for (const specId of specIds) {
2782
- const contractIds2 = specToContracts[specId] ?? [];
2783
- if (contractIds2.length === 0) {
2784
- lines.push(`- ${specId}: (none)`);
2785
- } else {
2786
- lines.push(`- ${specId}: ${contractIds2.join(", ")}`);
2787
- }
2929
+ const rows = specIds.map((specId) => {
2930
+ const entry = specToContracts[specId];
2931
+ const contracts = entry?.status === "missing" ? "(missing)" : entry && entry.ids.length > 0 ? entry.ids.join(", ") : "(none)";
2932
+ const status = entry?.status ?? "missing";
2933
+ return [specId, status, contracts];
2934
+ });
2935
+ lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
2936
+ }
2937
+ lines.push("");
2938
+ lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
2939
+ lines.push("");
2940
+ const missingRefSpecs = data.traceability.specs.missingRefSpecs;
2941
+ if (missingRefSpecs.length === 0) {
2942
+ lines.push("- (none)");
2943
+ } else {
2944
+ for (const specId of missingRefSpecs) {
2945
+ lines.push(`- ${specId}`);
2788
2946
  }
2789
2947
  }
2790
2948
  lines.push("");
2791
2949
  lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
2950
+ lines.push("");
2792
2951
  lines.push(`- total: ${data.traceability.sc.total}`);
2793
2952
  lines.push(`- covered: ${data.traceability.sc.covered}`);
2794
2953
  lines.push(`- missing: ${data.traceability.sc.missing}`);
@@ -2818,6 +2977,7 @@ function formatReportMarkdown(data) {
2818
2977
  }
2819
2978
  lines.push("");
2820
2979
  lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
2980
+ lines.push("");
2821
2981
  const scRefs = data.traceability.sc.refs;
2822
2982
  const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
2823
2983
  if (scIds.length === 0) {
@@ -2834,6 +2994,7 @@ function formatReportMarkdown(data) {
2834
2994
  }
2835
2995
  lines.push("");
2836
2996
  lines.push("## Spec:SC=1:1 \u9055\u53CD");
2997
+ lines.push("");
2837
2998
  const specScIssues = data.issues.filter(
2838
2999
  (item) => item.code === "QFAI-TRACE-012"
2839
3000
  );
@@ -2848,6 +3009,7 @@ function formatReportMarkdown(data) {
2848
3009
  }
2849
3010
  lines.push("");
2850
3011
  lines.push("## Hotspots");
3012
+ lines.push("");
2851
3013
  const hotspots = buildHotspots(data.issues);
2852
3014
  if (hotspots.length === 0) {
2853
3015
  lines.push("- (none)");
@@ -2860,6 +3022,7 @@ function formatReportMarkdown(data) {
2860
3022
  }
2861
3023
  lines.push("");
2862
3024
  lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
3025
+ lines.push("");
2863
3026
  const traceIssues = data.issues.filter(
2864
3027
  (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
2865
3028
  );
@@ -2875,6 +3038,7 @@ function formatReportMarkdown(data) {
2875
3038
  }
2876
3039
  lines.push("");
2877
3040
  lines.push("## \u691C\u8A3C\u7D50\u679C");
3041
+ lines.push("");
2878
3042
  if (data.issues.length === 0) {
2879
3043
  lines.push("- (none)");
2880
3044
  } else {
@@ -2891,29 +3055,41 @@ function formatReportMarkdown(data) {
2891
3055
  function formatReportJson(data) {
2892
3056
  return JSON.stringify(data, null, 2);
2893
3057
  }
2894
- async function collectSpecContractRefs(specFiles) {
2895
- const specToContractIds = /* @__PURE__ */ new Map();
3058
+ async function collectSpecContractRefs(specFiles, contractIdList) {
3059
+ const specToContracts = /* @__PURE__ */ new Map();
2896
3060
  const idToSpecs = /* @__PURE__ */ new Map();
2897
3061
  const missingRefSpecs = /* @__PURE__ */ new Set();
3062
+ for (const contractId of contractIdList) {
3063
+ idToSpecs.set(contractId, /* @__PURE__ */ new Set());
3064
+ }
2898
3065
  for (const file of specFiles) {
2899
3066
  const text = await (0, import_promises15.readFile)(file, "utf-8");
2900
3067
  const parsed = parseSpec(text, file);
2901
- const specKey = parsed.specId ?? file;
3068
+ const specKey = parsed.specId;
3069
+ if (!specKey) {
3070
+ continue;
3071
+ }
2902
3072
  const refs = parsed.contractRefs;
2903
3073
  if (refs.lines.length === 0) {
2904
3074
  missingRefSpecs.add(specKey);
3075
+ specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
3076
+ continue;
2905
3077
  }
2906
- const currentContracts = specToContractIds.get(specKey) ?? /* @__PURE__ */ new Set();
3078
+ const current = specToContracts.get(specKey) ?? {
3079
+ status: "declared",
3080
+ ids: /* @__PURE__ */ new Set()
3081
+ };
2907
3082
  for (const id of refs.ids) {
2908
- currentContracts.add(id);
2909
- const specs = idToSpecs.get(id) ?? /* @__PURE__ */ new Set();
2910
- specs.add(specKey);
2911
- idToSpecs.set(id, specs);
3083
+ current.ids.add(id);
3084
+ const specs = idToSpecs.get(id);
3085
+ if (specs) {
3086
+ specs.add(specKey);
3087
+ }
2912
3088
  }
2913
- specToContractIds.set(specKey, currentContracts);
3089
+ specToContracts.set(specKey, current);
2914
3090
  }
2915
3091
  return {
2916
- specToContractIds,
3092
+ specToContracts,
2917
3093
  idToSpecs,
2918
3094
  missingRefSpecs
2919
3095
  };
@@ -2990,6 +3166,20 @@ function formatList(values) {
2990
3166
  }
2991
3167
  return values.join(", ");
2992
3168
  }
3169
+ function formatMarkdownTable(headers, rows) {
3170
+ const widths = headers.map((header, index) => {
3171
+ const candidates = rows.map((row) => row[index] ?? "");
3172
+ return Math.max(header.length, ...candidates.map((item) => item.length));
3173
+ });
3174
+ const formatRow = (cells) => {
3175
+ const padded = cells.map(
3176
+ (cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)
3177
+ );
3178
+ return `| ${padded.join(" | ")} |`;
3179
+ };
3180
+ const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
3181
+ return [formatRow(headers), separator, ...rows.map(formatRow)];
3182
+ }
2993
3183
  function toSortedArray2(values) {
2994
3184
  return Array.from(values).sort((a, b) => a.localeCompare(b));
2995
3185
  }
@@ -3000,6 +3190,27 @@ function mapToSortedRecord(values) {
3000
3190
  }
3001
3191
  return record2;
3002
3192
  }
3193
+ function mapToSpecContractRecord(values) {
3194
+ const record2 = {};
3195
+ for (const [key, entry] of values.entries()) {
3196
+ record2[key] = {
3197
+ status: entry.status,
3198
+ ids: toSortedArray2(entry.ids)
3199
+ };
3200
+ }
3201
+ return record2;
3202
+ }
3203
+ function normalizeScSources(root, sources) {
3204
+ const normalized = /* @__PURE__ */ new Map();
3205
+ for (const [id, files] of sources.entries()) {
3206
+ const mapped = /* @__PURE__ */ new Set();
3207
+ for (const file of files) {
3208
+ mapped.add(toRelativePath(root, file));
3209
+ }
3210
+ normalized.set(id, mapped);
3211
+ }
3212
+ return normalized;
3213
+ }
3003
3214
  function buildHotspots(issues) {
3004
3215
  const map = /* @__PURE__ */ new Map();
3005
3216
  for (const issue7 of issues) {
@@ -3024,38 +3235,53 @@ function buildHotspots(issues) {
3024
3235
 
3025
3236
  // src/cli/commands/report.ts
3026
3237
  async function runReport(options) {
3027
- const root = import_node_path15.default.resolve(options.root);
3238
+ const root = import_node_path16.default.resolve(options.root);
3028
3239
  const configResult = await loadConfig(root);
3029
- const input = configResult.config.output.validateJsonPath;
3030
- const inputPath = import_node_path15.default.isAbsolute(input) ? input : import_node_path15.default.resolve(root, input);
3031
3240
  let validation;
3032
- try {
3033
- validation = await readValidationResult(inputPath);
3034
- } catch (err) {
3035
- if (isMissingFileError5(err)) {
3036
- error(
3037
- [
3038
- `qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
3039
- "",
3040
- "\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
3041
- " qfai validate",
3042
- "\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
3043
- "",
3044
- "GitHub Actions \u30C6\u30F3\u30D7\u30EC\u3092\u4F7F\u3063\u3066\u3044\u308B\u5834\u5408\u306F\u3001workflow \u306E validate \u30B8\u30E7\u30D6\u3092\u5148\u306B\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
3045
- ].join("\n")
3046
- );
3047
- process.exitCode = 2;
3048
- return;
3241
+ if (options.runValidate) {
3242
+ if (options.inputPath) {
3243
+ warn("report: --run-validate \u304C\u6307\u5B9A\u3055\u308C\u305F\u305F\u3081 --in \u306F\u7121\u8996\u3057\u307E\u3059\u3002");
3244
+ }
3245
+ const result = await validateProject(root, configResult);
3246
+ const normalized = normalizeValidationResult(root, result);
3247
+ await writeValidationResult(
3248
+ root,
3249
+ configResult.config.output.validateJsonPath,
3250
+ normalized
3251
+ );
3252
+ validation = normalized;
3253
+ } else {
3254
+ const input = options.inputPath ?? configResult.config.output.validateJsonPath;
3255
+ const inputPath = import_node_path16.default.isAbsolute(input) ? input : import_node_path16.default.resolve(root, input);
3256
+ try {
3257
+ validation = await readValidationResult(inputPath);
3258
+ } catch (err) {
3259
+ if (isMissingFileError5(err)) {
3260
+ error(
3261
+ [
3262
+ `qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
3263
+ "",
3264
+ "\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
3265
+ " qfai validate",
3266
+ "\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
3267
+ "",
3268
+ "\u307E\u305F\u306F report \u306B --run-validate \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
3269
+ "GitHub Actions \u30C6\u30F3\u30D7\u30EC\u3092\u4F7F\u3063\u3066\u3044\u308B\u5834\u5408\u306F\u3001workflow \u306E validate \u30B8\u30E7\u30D6\u3092\u5148\u306B\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
3270
+ ].join("\n")
3271
+ );
3272
+ process.exitCode = 2;
3273
+ return;
3274
+ }
3275
+ throw err;
3049
3276
  }
3050
- throw err;
3051
3277
  }
3052
3278
  const data = await createReportData(root, validation, configResult);
3053
3279
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
3054
3280
  const outRoot = resolvePath(root, configResult.config, "outDir");
3055
- const defaultOut = options.format === "json" ? import_node_path15.default.join(outRoot, "report.json") : import_node_path15.default.join(outRoot, "report.md");
3281
+ const defaultOut = options.format === "json" ? import_node_path16.default.join(outRoot, "report.json") : import_node_path16.default.join(outRoot, "report.md");
3056
3282
  const out = options.outPath ?? defaultOut;
3057
- const outPath = import_node_path15.default.isAbsolute(out) ? out : import_node_path15.default.resolve(root, out);
3058
- await (0, import_promises16.mkdir)(import_node_path15.default.dirname(outPath), { recursive: true });
3283
+ const outPath = import_node_path16.default.isAbsolute(out) ? out : import_node_path16.default.resolve(root, out);
3284
+ await (0, import_promises16.mkdir)(import_node_path16.default.dirname(outPath), { recursive: true });
3059
3285
  await (0, import_promises16.writeFile)(outPath, `${output}
3060
3286
  `, "utf-8");
3061
3287
  info(
@@ -3119,10 +3345,16 @@ function isMissingFileError5(error2) {
3119
3345
  const record2 = error2;
3120
3346
  return record2.code === "ENOENT";
3121
3347
  }
3348
+ async function writeValidationResult(root, outputPath, result) {
3349
+ const abs = import_node_path16.default.isAbsolute(outputPath) ? outputPath : import_node_path16.default.resolve(root, outputPath);
3350
+ await (0, import_promises16.mkdir)(import_node_path16.default.dirname(abs), { recursive: true });
3351
+ await (0, import_promises16.writeFile)(abs, `${JSON.stringify(result, null, 2)}
3352
+ `, "utf-8");
3353
+ }
3122
3354
 
3123
3355
  // src/cli/commands/validate.ts
3124
3356
  var import_promises17 = require("fs/promises");
3125
- var import_node_path16 = __toESM(require("path"), 1);
3357
+ var import_node_path17 = __toESM(require("path"), 1);
3126
3358
 
3127
3359
  // src/cli/lib/failOn.ts
3128
3360
  function shouldFail(result, failOn) {
@@ -3137,19 +3369,24 @@ function shouldFail(result, failOn) {
3137
3369
 
3138
3370
  // src/cli/commands/validate.ts
3139
3371
  async function runValidate(options) {
3140
- const root = import_node_path16.default.resolve(options.root);
3372
+ const root = import_node_path17.default.resolve(options.root);
3141
3373
  const configResult = await loadConfig(root);
3142
3374
  const result = await validateProject(root, configResult);
3375
+ const normalized = normalizeValidationResult(root, result);
3143
3376
  const format = options.format ?? "text";
3144
3377
  if (format === "text") {
3145
- emitText(result);
3378
+ emitText(normalized);
3146
3379
  }
3147
3380
  if (format === "github") {
3148
- result.issues.forEach(emitGitHub);
3381
+ const jsonPath = resolveJsonPath(
3382
+ root,
3383
+ configResult.config.output.validateJsonPath
3384
+ );
3385
+ emitGitHubOutput(normalized, root, jsonPath);
3149
3386
  }
3150
- await emitJson(result, root, configResult.config.output.validateJsonPath);
3387
+ await emitJson(normalized, root, configResult.config.output.validateJsonPath);
3151
3388
  const failOn = resolveFailOn(options, configResult.config.validation.failOn);
3152
- return shouldFail(result, failOn) ? 1 : 0;
3389
+ return shouldFail(normalized, failOn) ? 1 : 0;
3153
3390
  }
3154
3391
  function resolveFailOn(options, fallback) {
3155
3392
  if (options.failOn) {
@@ -3174,6 +3411,22 @@ function emitText(result) {
3174
3411
  `
3175
3412
  );
3176
3413
  }
3414
+ function emitGitHubOutput(result, root, jsonPath) {
3415
+ const deduped = dedupeIssues(result.issues);
3416
+ const omitted = Math.max(deduped.length - GITHUB_ANNOTATION_LIMIT, 0);
3417
+ const dropped = Math.max(result.issues.length - deduped.length, 0);
3418
+ emitGitHubSummary(result, {
3419
+ total: deduped.length,
3420
+ omitted,
3421
+ dropped,
3422
+ jsonPath,
3423
+ root
3424
+ });
3425
+ const issues = deduped.slice(0, GITHUB_ANNOTATION_LIMIT);
3426
+ for (const issue7 of issues) {
3427
+ emitGitHub(issue7);
3428
+ }
3429
+ }
3177
3430
  function emitGitHub(issue7) {
3178
3431
  const level = issue7.severity === "error" ? "error" : issue7.severity === "warning" ? "warning" : "notice";
3179
3432
  const file = issue7.file ? `file=${issue7.file}` : "";
@@ -3185,22 +3438,74 @@ function emitGitHub(issue7) {
3185
3438
  `
3186
3439
  );
3187
3440
  }
3441
+ function emitGitHubSummary(result, options) {
3442
+ const summary = [
3443
+ "qfai validate summary:",
3444
+ `error=${result.counts.error}`,
3445
+ `warning=${result.counts.warning}`,
3446
+ `info=${result.counts.info}`,
3447
+ `annotations=${Math.min(options.total, GITHUB_ANNOTATION_LIMIT)}/${options.total}`
3448
+ ].join(" ");
3449
+ process.stdout.write(`${summary}
3450
+ `);
3451
+ if (options.dropped > 0 || options.omitted > 0) {
3452
+ const details = [
3453
+ "qfai validate note:",
3454
+ options.dropped > 0 ? `\u91CD\u8907\u9664\u5916=${options.dropped}` : null,
3455
+ options.omitted > 0 ? `\u4E0A\u9650\u7701\u7565=${options.omitted}` : null
3456
+ ].filter(Boolean).join(" ");
3457
+ process.stdout.write(`${details}
3458
+ `);
3459
+ }
3460
+ const relative = toRelativePath(options.root, options.jsonPath);
3461
+ process.stdout.write(
3462
+ `qfai validate note: \u8A73\u7D30\u306F ${relative} \u307E\u305F\u306F --format text \u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\u3002
3463
+ `
3464
+ );
3465
+ }
3466
+ function dedupeIssues(issues) {
3467
+ const seen = /* @__PURE__ */ new Set();
3468
+ const deduped = [];
3469
+ for (const issue7 of issues) {
3470
+ const key = issueKey(issue7);
3471
+ if (seen.has(key)) {
3472
+ continue;
3473
+ }
3474
+ seen.add(key);
3475
+ deduped.push(issue7);
3476
+ }
3477
+ return deduped;
3478
+ }
3479
+ function issueKey(issue7) {
3480
+ const file = issue7.file ?? "";
3481
+ const line = issue7.loc?.line ?? "";
3482
+ const column = issue7.loc?.column ?? "";
3483
+ return [issue7.code, issue7.severity, issue7.message, file, line, column].join(
3484
+ "|"
3485
+ );
3486
+ }
3188
3487
  async function emitJson(result, root, jsonPath) {
3189
- const abs = import_node_path16.default.isAbsolute(jsonPath) ? jsonPath : import_node_path16.default.resolve(root, jsonPath);
3190
- await (0, import_promises17.mkdir)(import_node_path16.default.dirname(abs), { recursive: true });
3488
+ const abs = resolveJsonPath(root, jsonPath);
3489
+ await (0, import_promises17.mkdir)(import_node_path17.default.dirname(abs), { recursive: true });
3191
3490
  await (0, import_promises17.writeFile)(abs, `${JSON.stringify(result, null, 2)}
3192
3491
  `, "utf-8");
3193
3492
  }
3493
+ function resolveJsonPath(root, jsonPath) {
3494
+ return import_node_path17.default.isAbsolute(jsonPath) ? jsonPath : import_node_path17.default.resolve(root, jsonPath);
3495
+ }
3496
+ var GITHUB_ANNOTATION_LIMIT = 100;
3194
3497
 
3195
3498
  // src/cli/lib/args.ts
3196
3499
  function parseArgs(argv, cwd) {
3197
3500
  const options = {
3198
3501
  root: cwd,
3502
+ rootExplicit: false,
3199
3503
  dir: cwd,
3200
3504
  force: false,
3201
3505
  yes: false,
3202
3506
  dryRun: false,
3203
3507
  reportFormat: "md",
3508
+ reportRunValidate: false,
3204
3509
  validateFormat: "text",
3205
3510
  strict: false,
3206
3511
  help: false
@@ -3216,6 +3521,7 @@ function parseArgs(argv, cwd) {
3216
3521
  switch (arg) {
3217
3522
  case "--root":
3218
3523
  options.root = args[i + 1] ?? options.root;
3524
+ options.rootExplicit = true;
3219
3525
  i += 1;
3220
3526
  break;
3221
3527
  case "--dir":
@@ -3257,6 +3563,18 @@ function parseArgs(argv, cwd) {
3257
3563
  }
3258
3564
  i += 1;
3259
3565
  break;
3566
+ case "--in":
3567
+ {
3568
+ const next = args[i + 1];
3569
+ if (next) {
3570
+ options.reportIn = next;
3571
+ }
3572
+ }
3573
+ i += 1;
3574
+ break;
3575
+ case "--run-validate":
3576
+ options.reportRunValidate = true;
3577
+ break;
3260
3578
  case "--help":
3261
3579
  case "-h":
3262
3580
  options.help = true;
@@ -3308,19 +3626,27 @@ async function run(argv, cwd) {
3308
3626
  });
3309
3627
  return;
3310
3628
  case "validate":
3311
- process.exitCode = await runValidate({
3312
- root: options.root,
3313
- strict: options.strict,
3314
- format: options.validateFormat,
3315
- ...options.failOn !== void 0 ? { failOn: options.failOn } : {}
3316
- });
3629
+ {
3630
+ const resolvedRoot = await resolveRoot(options);
3631
+ process.exitCode = await runValidate({
3632
+ root: resolvedRoot,
3633
+ strict: options.strict,
3634
+ format: options.validateFormat,
3635
+ ...options.failOn !== void 0 ? { failOn: options.failOn } : {}
3636
+ });
3637
+ }
3317
3638
  return;
3318
3639
  case "report":
3319
- await runReport({
3320
- root: options.root,
3321
- format: options.reportFormat,
3322
- ...options.reportOut !== void 0 ? { outPath: options.reportOut } : {}
3323
- });
3640
+ {
3641
+ const resolvedRoot = await resolveRoot(options);
3642
+ await runReport({
3643
+ root: resolvedRoot,
3644
+ format: options.reportFormat,
3645
+ ...options.reportOut !== void 0 ? { outPath: options.reportOut } : {},
3646
+ ...options.reportIn !== void 0 ? { inputPath: options.reportIn } : {},
3647
+ ...options.reportRunValidate ? { runValidate: true } : {}
3648
+ });
3649
+ }
3324
3650
  return;
3325
3651
  default:
3326
3652
  error(`Unknown command: ${command}`);
@@ -3347,9 +3673,23 @@ Options:
3347
3673
  --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
3348
3674
  --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
3349
3675
  --out <path> report: \u51FA\u529B\u5148
3676
+ --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
3677
+ --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
3350
3678
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A
3351
3679
  `;
3352
3680
  }
3681
+ async function resolveRoot(options) {
3682
+ if (options.rootExplicit) {
3683
+ return options.root;
3684
+ }
3685
+ const search = await findConfigRoot(options.root);
3686
+ if (!search.found) {
3687
+ warn(
3688
+ `qfai: qfai.config.yaml \u304C\u898B\u3064\u304B\u3089\u306A\u3044\u305F\u3081 defaultConfig \u3092\u4F7F\u7528\u3057\u307E\u3059 (root=${search.root})`
3689
+ );
3690
+ }
3691
+ return search.root;
3692
+ }
3353
3693
 
3354
3694
  // src/cli/index.ts
3355
3695
  run(process.argv.slice(2), process.cwd()).catch((err) => {