speckeeper 0.9.1 → 0.9.3

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/cli.js CHANGED
@@ -939,6 +939,8 @@ async function checkCommand(type, options) {
939
939
  console.log(chalk4.gray(` Sources: ${sources.length} configured`));
940
940
  }
941
941
  const results = [];
942
+ const transitiveRelations = config.coverage?.transitiveRelations ?? [];
943
+ let transitiveCoverageData;
942
944
  const allSpecIds = [];
943
945
  const specIdToModel = /* @__PURE__ */ new Map();
944
946
  const lookupKeyMap = /* @__PURE__ */ new Map();
@@ -1010,9 +1012,20 @@ async function checkCommand(type, options) {
1010
1012
  }
1011
1013
  }
1012
1014
  }
1015
+ const directlyCovered = new Set(matches.keys());
1016
+ let coveredSet = directlyCovered;
1017
+ if (transitiveRelations.length > 0) {
1018
+ const allSpecsWithRelations = allSpecIds.map((id) => {
1019
+ const entry = specIdToModel.get(id);
1020
+ const spec = entry?.spec;
1021
+ return { id, relations: spec?.relations };
1022
+ });
1023
+ transitiveCoverageData = computeTransitiveCoverage(directlyCovered, allSpecsWithRelations, transitiveRelations);
1024
+ coveredSet = transitiveCoverageData.coveredSet;
1025
+ }
1013
1026
  if (options.verbose) {
1014
1027
  for (const specId of allSpecIds) {
1015
- if (!matches.has(specId)) {
1028
+ if (!coveredSet.has(specId)) {
1016
1029
  const entry = specIdToModel.get(specId);
1017
1030
  results.push({
1018
1031
  type: entry?.modelId ?? "unknown",
@@ -1048,17 +1061,47 @@ async function checkCommand(type, options) {
1048
1061
  });
1049
1062
  }
1050
1063
  }
1064
+ }
1065
+ if (transitiveCoverageData && transitiveRelations.length > 0) {
1066
+ const total = allSpecIds.length;
1067
+ const directCount = transitiveCoverageData.directCount;
1068
+ const transitiveCount = transitiveCoverageData.transitiveCount;
1069
+ const covered = directCount + transitiveCount;
1070
+ const uncovered = total - covered;
1071
+ const coveragePercent = total > 0 ? Math.round(covered / total * 100) : 100;
1051
1072
  console.log("");
1052
- console.log(chalk4.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1053
- const allPassed = coverageResults.every((c) => c.result.coveragePercent >= 80);
1054
- if (allPassed) {
1055
- console.log(chalk4.green(" \u2713 All coverage checks passed (\u226580%)"));
1056
- } else {
1057
- const failed = coverageResults.filter((c) => c.result.coveragePercent < 80);
1058
- console.log(chalk4.yellow(` \u26A0 ${failed.length} coverage check(s) below 80%`));
1073
+ console.log(chalk4.blue(` Transitive coverage (via ${transitiveRelations.join(", ")})`));
1074
+ console.log(chalk4.gray(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
1075
+ console.log(chalk4.gray(` Total: ${total}`));
1076
+ console.log(chalk4.green(` Covered: ${covered} (${directCount} direct + ${transitiveCount} transitive)`));
1077
+ console.log(chalk4.yellow(` Uncovered: ${uncovered}`));
1078
+ const color = coveragePercent >= 80 ? chalk4.green : coveragePercent >= 50 ? chalk4.yellow : chalk4.red;
1079
+ console.log(color(` Coverage: ${coveragePercent}%`));
1080
+ if (uncovered > 0) {
1081
+ const uncoveredIds = allSpecIds.filter((id) => !transitiveCoverageData.coveredSet.has(id));
1082
+ const display = uncoveredIds.slice(0, 10);
1083
+ console.log("");
1084
+ console.log(chalk4.yellow(" Uncovered items:"));
1085
+ for (const id of display) {
1086
+ const entry = specIdToModel.get(id);
1087
+ console.log(chalk4.yellow(` - ${id} (${entry?.modelId ?? "unknown"})`));
1088
+ }
1089
+ if (uncoveredIds.length > 10) {
1090
+ console.log(chalk4.yellow(` ... and ${uncoveredIds.length - 10} more`));
1091
+ }
1059
1092
  }
1060
- } else {
1093
+ }
1094
+ console.log("");
1095
+ console.log(chalk4.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1096
+ const allCoverageResults = [...coverageResults];
1097
+ const allPassed = allCoverageResults.every((c) => c.result.coveragePercent >= 80);
1098
+ if (coverageResults.length === 0 && !transitiveCoverageData) {
1061
1099
  console.log(chalk4.gray(" No coverage checker found"));
1100
+ } else if (allPassed) {
1101
+ console.log(chalk4.green(" \u2713 All coverage checks passed (\u226580%)"));
1102
+ } else {
1103
+ const failed = allCoverageResults.filter((c) => c.result.coveragePercent < 80);
1104
+ console.log(chalk4.yellow(` \u26A0 ${failed.length} coverage check(s) below 80%`));
1062
1105
  }
1063
1106
  }
1064
1107
  const hasErrors = results.some((r) => !r.success);
@@ -1070,6 +1113,38 @@ async function checkCommand(type, options) {
1070
1113
  process.exit(1);
1071
1114
  }
1072
1115
  }
1116
+ function computeTransitiveCoverage(directlyCovered, allSpecs, transitiveRelations) {
1117
+ if (transitiveRelations.length === 0) {
1118
+ return { coveredSet: new Set(directlyCovered), directCount: directlyCovered.size, transitiveCount: 0 };
1119
+ }
1120
+ const reverseRelations = /* @__PURE__ */ new Map();
1121
+ for (const spec of allSpecs) {
1122
+ if (!spec.relations) continue;
1123
+ for (const rel of spec.relations) {
1124
+ if (!transitiveRelations.includes(rel.type)) continue;
1125
+ const existing = reverseRelations.get(rel.target) ?? [];
1126
+ existing.push(spec.id);
1127
+ reverseRelations.set(rel.target, existing);
1128
+ }
1129
+ }
1130
+ const coveredSet = new Set(directlyCovered);
1131
+ let changed = true;
1132
+ while (changed) {
1133
+ changed = false;
1134
+ for (const [targetId, sourceIds] of reverseRelations) {
1135
+ if (coveredSet.has(targetId)) continue;
1136
+ if (sourceIds.length > 0 && sourceIds.every((id) => coveredSet.has(id))) {
1137
+ coveredSet.add(targetId);
1138
+ changed = true;
1139
+ }
1140
+ }
1141
+ }
1142
+ return {
1143
+ coveredSet,
1144
+ directCount: directlyCovered.size,
1145
+ transitiveCount: coveredSet.size - directlyCovered.size
1146
+ };
1147
+ }
1073
1148
  function loadExternalData(filePath) {
1074
1149
  const content = readFileSync(filePath, "utf-8");
1075
1150
  if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {