@schemasentry/cli 0.3.2 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +29 -0
  2. package/dist/index.js +430 -14
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -56,6 +56,31 @@ pnpm schemasentry audit \
56
56
  --output ./report.html
57
57
  ```
58
58
 
59
+ ### `collect`
60
+
61
+ Collect JSON-LD blocks from built HTML output and emit schema data JSON:
62
+
63
+ ```bash
64
+ pnpm schemasentry collect --root ./out --output ./schema-sentry.data.json
65
+ ```
66
+
67
+ Check collected output against your current data file (CI drift guard):
68
+
69
+ ```bash
70
+ pnpm schemasentry collect --root ./out --check --data ./schema-sentry.data.json
71
+ ```
72
+
73
+ Collect and compare only selected routes, failing if any required route is missing:
74
+
75
+ ```bash
76
+ pnpm schemasentry collect \
77
+ --root ./out \
78
+ --routes / /blog /faq \
79
+ --strict-routes \
80
+ --check \
81
+ --data ./schema-sentry.data.json
82
+ ```
83
+
59
84
  ## Options
60
85
 
61
86
  | Option | Description |
@@ -63,6 +88,10 @@ pnpm schemasentry audit \
63
88
  | `--format json\|html` | Output format |
64
89
  | `--annotations none\|github` | CI annotations |
65
90
  | `-o, --output <path>` | Write output to file |
91
+ | `--root <path>` | Root directory to scan for HTML output (`collect`) |
92
+ | `--routes <routes...>` | Collect only specific routes (`collect`) |
93
+ | `--strict-routes` | Fail when any route passed to `--routes` is missing (`collect`) |
94
+ | `--check` | Compare collected output with existing data and fail on drift (`collect`) |
66
95
  | `--recommended / --no-recommended` | Enable recommended field checks |
67
96
 
68
97
  ## Documentation
package/dist/index.js CHANGED
@@ -4,8 +4,8 @@
4
4
  import { Command } from "commander";
5
5
  import { mkdir, readFile, writeFile } from "fs/promises";
6
6
  import { readFileSync } from "fs";
7
- import path4 from "path";
8
- import { stableStringify as stableStringify2 } from "@schemasentry/core";
7
+ import path5 from "path";
8
+ import { stableStringify as stableStringify3 } from "@schemasentry/core";
9
9
 
10
10
  // src/report.ts
11
11
  import {
@@ -654,6 +654,229 @@ var emitGitHubAnnotations = (report, commandLabel) => {
654
654
  }
655
655
  };
656
656
 
657
+ // src/collect.ts
658
+ import { promises as fs4 } from "fs";
659
+ import path4 from "path";
660
+ import { stableStringify as stableStringify2 } from "@schemasentry/core";
661
+ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules", ".pnpm-store"]);
662
+ var SCRIPT_TAG_REGEX = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
663
+ var JSON_LD_TYPE_REGEX = /\btype\s*=\s*(?:"application\/ld\+json"|'application\/ld\+json'|application\/ld\+json)/i;
664
+ var collectSchemaData = async (options) => {
665
+ const rootDir = path4.resolve(options.rootDir);
666
+ const requestedRoutes = normalizeRouteFilter(options.routes ?? []);
667
+ const htmlFiles = (await walkHtmlFiles(rootDir)).sort((a, b) => a.localeCompare(b));
668
+ const routes = {};
669
+ const warnings = [];
670
+ let blockCount = 0;
671
+ let invalidBlocks = 0;
672
+ for (const filePath of htmlFiles) {
673
+ const route = filePathToRoute(rootDir, filePath);
674
+ if (!route) {
675
+ continue;
676
+ }
677
+ const html = await fs4.readFile(filePath, "utf8");
678
+ const extracted = extractSchemaNodes(html, filePath);
679
+ if (extracted.nodes.length > 0) {
680
+ routes[route] = [...routes[route] ?? [], ...extracted.nodes];
681
+ blockCount += extracted.nodes.length;
682
+ }
683
+ invalidBlocks += extracted.invalidBlocks;
684
+ warnings.push(...extracted.warnings);
685
+ }
686
+ const missingRoutes = [];
687
+ const filteredRoutes = requestedRoutes.length > 0 ? filterRoutesByAllowlist(routes, requestedRoutes) : routes;
688
+ if (requestedRoutes.length > 0) {
689
+ for (const route of requestedRoutes) {
690
+ if (!Object.prototype.hasOwnProperty.call(filteredRoutes, route)) {
691
+ missingRoutes.push(route);
692
+ }
693
+ }
694
+ }
695
+ const filteredBlockCount = Object.values(filteredRoutes).reduce(
696
+ (total, nodes) => total + nodes.length,
697
+ 0
698
+ );
699
+ return {
700
+ data: {
701
+ routes: sortRoutes(filteredRoutes)
702
+ },
703
+ stats: {
704
+ htmlFiles: htmlFiles.length,
705
+ routes: Object.keys(filteredRoutes).length,
706
+ blocks: filteredBlockCount,
707
+ invalidBlocks
708
+ },
709
+ warnings,
710
+ requestedRoutes,
711
+ missingRoutes
712
+ };
713
+ };
714
+ var compareSchemaData = (existing, collected) => {
715
+ const existingRoutes = existing.routes ?? {};
716
+ const collectedRoutes = collected.routes ?? {};
717
+ const existingKeys = Object.keys(existingRoutes);
718
+ const collectedKeys = Object.keys(collectedRoutes);
719
+ const addedRoutes = collectedKeys.filter((route) => !Object.prototype.hasOwnProperty.call(existingRoutes, route)).sort();
720
+ const removedRoutes = existingKeys.filter((route) => !Object.prototype.hasOwnProperty.call(collectedRoutes, route)).sort();
721
+ const changedRoutes = existingKeys.filter((route) => Object.prototype.hasOwnProperty.call(collectedRoutes, route)).filter(
722
+ (route) => stableStringify2(existingRoutes[route]) !== stableStringify2(collectedRoutes[route])
723
+ ).sort();
724
+ const changedRouteDetails = changedRoutes.map(
725
+ (route) => buildRouteDriftDetail(route, existingRoutes[route] ?? [], collectedRoutes[route] ?? [])
726
+ );
727
+ return {
728
+ hasChanges: addedRoutes.length > 0 || removedRoutes.length > 0 || changedRoutes.length > 0,
729
+ addedRoutes,
730
+ removedRoutes,
731
+ changedRoutes,
732
+ changedRouteDetails
733
+ };
734
+ };
735
+ var formatSchemaDataDrift = (drift, maxRoutes = 5) => {
736
+ if (!drift.hasChanges) {
737
+ return "No schema data drift detected.";
738
+ }
739
+ const lines = [
740
+ `Schema data drift detected: added_routes=${drift.addedRoutes.length} removed_routes=${drift.removedRoutes.length} changed_routes=${drift.changedRoutes.length}`
741
+ ];
742
+ if (drift.addedRoutes.length > 0) {
743
+ lines.push(formatRoutePreview("Added routes", drift.addedRoutes, maxRoutes));
744
+ }
745
+ if (drift.removedRoutes.length > 0) {
746
+ lines.push(formatRoutePreview("Removed routes", drift.removedRoutes, maxRoutes));
747
+ }
748
+ if (drift.changedRoutes.length > 0) {
749
+ lines.push(formatRoutePreview("Changed routes", drift.changedRoutes, maxRoutes));
750
+ const details = drift.changedRouteDetails.slice(0, maxRoutes).map((detail) => formatRouteDriftDetail(detail));
751
+ if (details.length > 0) {
752
+ lines.push("Changed route details:");
753
+ for (const detail of details) {
754
+ lines.push(`- ${detail}`);
755
+ }
756
+ }
757
+ }
758
+ return lines.join("\n");
759
+ };
760
+ var formatRoutePreview = (label, routes, maxRoutes) => {
761
+ const preview = routes.slice(0, maxRoutes);
762
+ const suffix = routes.length > maxRoutes ? ` (+${routes.length - maxRoutes} more)` : "";
763
+ return `${label}: ${preview.join(", ")}${suffix}`;
764
+ };
765
+ var formatRouteDriftDetail = (detail) => {
766
+ const added = detail.addedTypes.length > 0 ? detail.addedTypes.join(",") : "(none)";
767
+ const removed = detail.removedTypes.length > 0 ? detail.removedTypes.join(",") : "(none)";
768
+ return `${detail.route} blocks ${detail.beforeBlocks}->${detail.afterBlocks} | +types ${added} | -types ${removed}`;
769
+ };
770
+ var sortRoutes = (routes) => Object.fromEntries(
771
+ Object.entries(routes).sort(([a], [b]) => a.localeCompare(b))
772
+ );
773
+ var filterRoutesByAllowlist = (routes, allowlist) => {
774
+ const filtered = {};
775
+ for (const route of allowlist) {
776
+ if (Object.prototype.hasOwnProperty.call(routes, route)) {
777
+ filtered[route] = routes[route];
778
+ }
779
+ }
780
+ return filtered;
781
+ };
782
+ var normalizeRouteFilter = (input2) => {
783
+ const normalized = input2.flatMap((entry) => entry.split(",")).map((route) => route.trim()).filter((route) => route.length > 0);
784
+ return Array.from(new Set(normalized)).sort();
785
+ };
786
+ var walkHtmlFiles = async (rootDir) => {
787
+ const entries = await fs4.readdir(rootDir, { withFileTypes: true });
788
+ const files = [];
789
+ for (const entry of entries) {
790
+ if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) {
791
+ continue;
792
+ }
793
+ const resolved = path4.join(rootDir, entry.name);
794
+ if (entry.isDirectory()) {
795
+ files.push(...await walkHtmlFiles(resolved));
796
+ continue;
797
+ }
798
+ if (entry.isFile() && entry.name.endsWith(".html")) {
799
+ files.push(resolved);
800
+ }
801
+ }
802
+ return files;
803
+ };
804
+ var filePathToRoute = (rootDir, filePath) => {
805
+ const relative = path4.relative(rootDir, filePath).replace(/\\/g, "/");
806
+ if (relative === "index.html") {
807
+ return "/";
808
+ }
809
+ if (relative.endsWith("/index.html")) {
810
+ return `/${relative.slice(0, -"/index.html".length)}`;
811
+ }
812
+ if (relative.endsWith(".html")) {
813
+ return `/${relative.slice(0, -".html".length)}`;
814
+ }
815
+ return null;
816
+ };
817
+ var extractSchemaNodes = (html, filePath) => {
818
+ const nodes = [];
819
+ const warnings = [];
820
+ let invalidBlocks = 0;
821
+ let scriptIndex = 0;
822
+ for (const match of html.matchAll(SCRIPT_TAG_REGEX)) {
823
+ scriptIndex += 1;
824
+ const attributes = match[1] ?? "";
825
+ if (!JSON_LD_TYPE_REGEX.test(attributes)) {
826
+ continue;
827
+ }
828
+ const scriptBody = (match[2] ?? "").trim();
829
+ if (!scriptBody) {
830
+ continue;
831
+ }
832
+ let parsed;
833
+ try {
834
+ parsed = JSON.parse(scriptBody);
835
+ } catch {
836
+ invalidBlocks += 1;
837
+ warnings.push({
838
+ file: filePath,
839
+ message: `Invalid JSON-LD block at script #${scriptIndex}`
840
+ });
841
+ continue;
842
+ }
843
+ const normalized = normalizeParsedBlock(parsed);
844
+ nodes.push(...normalized);
845
+ }
846
+ return { nodes, invalidBlocks, warnings };
847
+ };
848
+ var normalizeParsedBlock = (value) => {
849
+ if (Array.isArray(value)) {
850
+ return value.filter(isJsonObject);
851
+ }
852
+ if (!isJsonObject(value)) {
853
+ return [];
854
+ }
855
+ const graph = value["@graph"];
856
+ if (Array.isArray(graph)) {
857
+ return graph.filter(isJsonObject);
858
+ }
859
+ return [value];
860
+ };
861
+ var isJsonObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
862
+ var buildRouteDriftDetail = (route, beforeNodes, afterNodes) => {
863
+ const beforeTypes = new Set(beforeNodes.map((node) => schemaTypeLabel(node)));
864
+ const afterTypes = new Set(afterNodes.map((node) => schemaTypeLabel(node)));
865
+ const addedTypes = Array.from(afterTypes).filter((type) => !beforeTypes.has(type)).sort();
866
+ const removedTypes = Array.from(beforeTypes).filter((type) => !afterTypes.has(type)).sort();
867
+ return {
868
+ route,
869
+ beforeBlocks: beforeNodes.length,
870
+ afterBlocks: afterNodes.length,
871
+ addedTypes,
872
+ removedTypes
873
+ };
874
+ };
875
+ var schemaTypeLabel = (node) => {
876
+ const type = node["@type"];
877
+ return typeof type === "string" && type.trim().length > 0 ? type : "(unknown)";
878
+ };
879
+
657
880
  // src/index.ts
658
881
  import { createInterface } from "readline/promises";
659
882
  import { stdin as input, stdout as output } from "process";
@@ -672,8 +895,8 @@ program.command("validate").description("Validate schema coverage and rules").op
672
895
  const format = resolveOutputFormat(options.format);
673
896
  const annotationsMode = resolveAnnotationsMode(options.annotations);
674
897
  const recommended = await resolveRecommendedOption(options.config);
675
- const manifestPath = path4.resolve(process.cwd(), options.manifest);
676
- const dataPath = path4.resolve(process.cwd(), options.data);
898
+ const manifestPath = path5.resolve(process.cwd(), options.manifest);
899
+ const dataPath = path5.resolve(process.cwd(), options.data);
677
900
  let raw;
678
901
  try {
679
902
  raw = await readFile(manifestPath, "utf8");
@@ -760,12 +983,12 @@ program.command("init").description("Interactive setup wizard").option(
760
983
  "Path to schema data JSON",
761
984
  "schema-sentry.data.json"
762
985
  ).option("-y, --yes", "Use defaults and skip prompts").option("-f, --force", "Overwrite existing files").option("--scan", "Scan the filesystem for routes and add WebPage entries").option("--root <path>", "Project root for scanning", ".").action(async (options) => {
763
- const manifestPath = path4.resolve(process.cwd(), options.manifest);
764
- const dataPath = path4.resolve(process.cwd(), options.data);
986
+ const manifestPath = path5.resolve(process.cwd(), options.manifest);
987
+ const dataPath = path5.resolve(process.cwd(), options.data);
765
988
  const force = options.force ?? false;
766
989
  const useDefaults = options.yes ?? false;
767
990
  const answers = useDefaults ? getDefaultAnswers() : await promptAnswers();
768
- const scannedRoutes = options.scan ? await scanRoutes({ rootDir: path4.resolve(process.cwd(), options.root ?? ".") }) : [];
991
+ const scannedRoutes = options.scan ? await scanRoutes({ rootDir: path5.resolve(process.cwd(), options.root ?? ".") }) : [];
769
992
  if (options.scan && scannedRoutes.length === 0) {
770
993
  console.error("No routes found during scan.");
771
994
  }
@@ -794,7 +1017,7 @@ program.command("audit").description("Analyze schema health and report issues").
794
1017
  const format = resolveOutputFormat(options.format);
795
1018
  const annotationsMode = resolveAnnotationsMode(options.annotations);
796
1019
  const recommended = await resolveRecommendedOption(options.config);
797
- const dataPath = path4.resolve(process.cwd(), options.data);
1020
+ const dataPath = path5.resolve(process.cwd(), options.data);
798
1021
  let dataRaw;
799
1022
  try {
800
1023
  dataRaw = await readFile(dataPath, "utf8");
@@ -830,7 +1053,7 @@ program.command("audit").description("Analyze schema health and report issues").
830
1053
  }
831
1054
  let manifest;
832
1055
  if (options.manifest) {
833
- const manifestPath = path4.resolve(process.cwd(), options.manifest);
1056
+ const manifestPath = path5.resolve(process.cwd(), options.manifest);
834
1057
  let manifestRaw;
835
1058
  try {
836
1059
  manifestRaw = await readFile(manifestPath, "utf8");
@@ -864,7 +1087,7 @@ program.command("audit").description("Analyze schema health and report issues").
864
1087
  return;
865
1088
  }
866
1089
  }
867
- const requiredRoutes = options.scan ? await scanRoutes({ rootDir: path4.resolve(process.cwd(), options.root ?? ".") }) : [];
1090
+ const requiredRoutes = options.scan ? await scanRoutes({ rootDir: path5.resolve(process.cwd(), options.root ?? ".") }) : [];
868
1091
  if (options.scan && requiredRoutes.length === 0) {
869
1092
  console.error("No routes found during scan.");
870
1093
  }
@@ -883,6 +1106,125 @@ program.command("audit").description("Analyze schema health and report issues").
883
1106
  printAuditSummary(report, Boolean(manifest), Date.now() - start);
884
1107
  process.exit(report.ok ? 0 : 1);
885
1108
  });
1109
+ program.command("collect").description("Collect JSON-LD blocks from built HTML output").option("--root <path>", "Root directory to scan for HTML files", ".").option("--routes <routes...>", "Only collect specific routes (repeat or comma-separated)").option("--strict-routes", "Fail when any route passed via --routes is missing").option("--format <format>", "Output format (json)", "json").option("-o, --output <path>", "Write collected schema data to file").option("--check", "Compare collected output with an existing schema data file").option(
1110
+ "-d, --data <path>",
1111
+ "Path to existing schema data JSON for --check",
1112
+ "schema-sentry.data.json"
1113
+ ).action(async (options) => {
1114
+ const start = Date.now();
1115
+ const format = resolveCollectOutputFormat(options.format);
1116
+ const rootDir = path5.resolve(process.cwd(), options.root ?? ".");
1117
+ const check = options.check ?? false;
1118
+ const requestedRoutes = normalizeRouteFilter(options.routes ?? []);
1119
+ const strictRoutes = options.strictRoutes ?? false;
1120
+ let collected;
1121
+ try {
1122
+ collected = await collectSchemaData({ rootDir, routes: requestedRoutes });
1123
+ } catch (error) {
1124
+ const reason = error instanceof Error && error.message.length > 0 ? error.message : "Unknown file system error";
1125
+ printCliError(
1126
+ "collect.scan_failed",
1127
+ `Could not scan HTML output at ${rootDir}: ${reason}`,
1128
+ "Point --root to a directory containing built HTML output."
1129
+ );
1130
+ process.exit(1);
1131
+ return;
1132
+ }
1133
+ if (collected.stats.htmlFiles === 0) {
1134
+ printCliError(
1135
+ "collect.no_html",
1136
+ `No HTML files found under ${rootDir}`,
1137
+ "Point --root to a static output directory (for example ./out)."
1138
+ );
1139
+ process.exit(1);
1140
+ return;
1141
+ }
1142
+ if (strictRoutes && collected.missingRoutes.length > 0) {
1143
+ printCliError(
1144
+ "collect.missing_required_routes",
1145
+ `Required routes were not found in collected HTML: ${collected.missingRoutes.join(", ")}`,
1146
+ "Rebuild output, adjust --root, or update --routes."
1147
+ );
1148
+ process.exit(1);
1149
+ return;
1150
+ }
1151
+ let driftDetected = false;
1152
+ if (check) {
1153
+ const existingPath = path5.resolve(process.cwd(), options.data);
1154
+ let existingRaw;
1155
+ try {
1156
+ existingRaw = await readFile(existingPath, "utf8");
1157
+ } catch (error) {
1158
+ printCliError(
1159
+ "data.not_found",
1160
+ `Schema data not found at ${existingPath}`,
1161
+ "Run `schemasentry collect --output ./schema-sentry.data.json` to generate it."
1162
+ );
1163
+ process.exit(1);
1164
+ return;
1165
+ }
1166
+ let existingData;
1167
+ try {
1168
+ existingData = JSON.parse(existingRaw);
1169
+ } catch (error) {
1170
+ printCliError(
1171
+ "data.invalid_json",
1172
+ "Schema data is not valid JSON",
1173
+ "Check the JSON syntax or regenerate with `schemasentry collect --output`."
1174
+ );
1175
+ process.exit(1);
1176
+ return;
1177
+ }
1178
+ if (!isSchemaData(existingData)) {
1179
+ printCliError(
1180
+ "data.invalid_shape",
1181
+ "Schema data must contain a 'routes' object with array values",
1182
+ "Ensure each route maps to an array of JSON-LD blocks."
1183
+ );
1184
+ process.exit(1);
1185
+ return;
1186
+ }
1187
+ const existingDataForCompare = requestedRoutes.length > 0 ? filterSchemaDataByRoutes(existingData, requestedRoutes) : existingData;
1188
+ const drift = compareSchemaData(existingDataForCompare, collected.data);
1189
+ driftDetected = drift.hasChanges;
1190
+ if (driftDetected) {
1191
+ console.error(formatSchemaDataDrift(drift));
1192
+ } else {
1193
+ console.error("collect | No schema data drift detected.");
1194
+ }
1195
+ }
1196
+ const content = formatCollectOutput(collected.data, format);
1197
+ if (options.output) {
1198
+ const resolvedPath = path5.resolve(process.cwd(), options.output);
1199
+ try {
1200
+ await mkdir(path5.dirname(resolvedPath), { recursive: true });
1201
+ await writeFile(resolvedPath, `${content}
1202
+ `, "utf8");
1203
+ console.error(`Collected data written to ${resolvedPath}`);
1204
+ } catch (error) {
1205
+ const reason = error instanceof Error && error.message.length > 0 ? error.message : "Unknown file system error";
1206
+ printCliError(
1207
+ "output.write_failed",
1208
+ `Could not write collected data to ${resolvedPath}: ${reason}`
1209
+ );
1210
+ process.exit(1);
1211
+ return;
1212
+ }
1213
+ } else if (!check) {
1214
+ console.log(content);
1215
+ }
1216
+ printCollectWarnings(collected.warnings);
1217
+ printCollectSummary({
1218
+ stats: collected.stats,
1219
+ durationMs: Date.now() - start,
1220
+ checked: check,
1221
+ driftDetected,
1222
+ requestedRoutes: collected.requestedRoutes,
1223
+ missingRoutes: collected.missingRoutes,
1224
+ strictRoutes
1225
+ });
1226
+ process.exit(driftDetected ? 1 : 0);
1227
+ });
886
1228
  function isManifest(value) {
887
1229
  if (!value || typeof value !== "object") {
888
1230
  return false;
@@ -939,11 +1281,30 @@ function resolveAnnotationsMode(value) {
939
1281
  process.exit(1);
940
1282
  return "none";
941
1283
  }
1284
+ function resolveCollectOutputFormat(value) {
1285
+ const format = (value ?? "json").trim().toLowerCase();
1286
+ if (format === "json") {
1287
+ return format;
1288
+ }
1289
+ printCliError(
1290
+ "output.invalid_format",
1291
+ `Unsupported collect output format '${value ?? ""}'`,
1292
+ "Use --format json."
1293
+ );
1294
+ process.exit(1);
1295
+ return "json";
1296
+ }
942
1297
  function formatReportOutput(report, format, title) {
943
1298
  if (format === "html") {
944
1299
  return renderHtmlReport(report, { title });
945
1300
  }
946
- return stableStringify2(report);
1301
+ return stableStringify3(report);
1302
+ }
1303
+ function formatCollectOutput(data, format) {
1304
+ if (format === "json") {
1305
+ return stableStringify3(data);
1306
+ }
1307
+ return stableStringify3(data);
947
1308
  }
948
1309
  async function emitReport(options) {
949
1310
  const { report, format, outputPath, title } = options;
@@ -952,9 +1313,9 @@ async function emitReport(options) {
952
1313
  console.log(content);
953
1314
  return;
954
1315
  }
955
- const resolvedPath = path4.resolve(process.cwd(), outputPath);
1316
+ const resolvedPath = path5.resolve(process.cwd(), outputPath);
956
1317
  try {
957
- await mkdir(path4.dirname(resolvedPath), { recursive: true });
1318
+ await mkdir(path5.dirname(resolvedPath), { recursive: true });
958
1319
  await writeFile(resolvedPath, content, "utf8");
959
1320
  console.error(`Report written to ${resolvedPath}`);
960
1321
  } catch (error) {
@@ -974,7 +1335,7 @@ function emitAnnotations(report, mode, commandLabel) {
974
1335
  }
975
1336
  function printCliError(code, message, suggestion) {
976
1337
  console.error(
977
- stableStringify2({
1338
+ stableStringify3({
978
1339
  ok: false,
979
1340
  errors: [
980
1341
  {
@@ -1040,6 +1401,61 @@ function printAuditSummary(report, coverageEnabled, durationMs) {
1040
1401
  console.error("Coverage checks skipped (no manifest provided).");
1041
1402
  }
1042
1403
  }
1404
+ function printCollectWarnings(warnings) {
1405
+ if (warnings.length === 0) {
1406
+ return;
1407
+ }
1408
+ const maxPrinted = 10;
1409
+ console.error(`collect | Warnings: ${warnings.length}`);
1410
+ for (const warning of warnings.slice(0, maxPrinted)) {
1411
+ console.error(`- ${warning.file}: ${warning.message}`);
1412
+ }
1413
+ if (warnings.length > maxPrinted) {
1414
+ console.error(`- ... ${warnings.length - maxPrinted} more warning(s)`);
1415
+ }
1416
+ }
1417
+ function printCollectSummary(options) {
1418
+ const {
1419
+ stats,
1420
+ durationMs,
1421
+ checked,
1422
+ driftDetected,
1423
+ requestedRoutes,
1424
+ missingRoutes,
1425
+ strictRoutes
1426
+ } = options;
1427
+ const parts = [
1428
+ `HTML files: ${stats.htmlFiles}`,
1429
+ `Routes: ${stats.routes}`,
1430
+ `Blocks: ${stats.blocks}`,
1431
+ `Invalid blocks: ${stats.invalidBlocks}`,
1432
+ `Duration: ${formatDuration(durationMs)}`
1433
+ ];
1434
+ if (checked) {
1435
+ parts.push(`Check: ${driftDetected ? "drift_detected" : "clean"}`);
1436
+ }
1437
+ if (requestedRoutes.length > 0) {
1438
+ parts.push(`Route filter: ${requestedRoutes.length}`);
1439
+ }
1440
+ if (missingRoutes.length > 0) {
1441
+ parts.push(`Missing filtered routes: ${missingRoutes.length}`);
1442
+ }
1443
+ if (strictRoutes) {
1444
+ parts.push("Strict routes: enabled");
1445
+ }
1446
+ console.error(`collect | ${parts.join(" | ")}`);
1447
+ }
1448
+ function filterSchemaDataByRoutes(data, routes) {
1449
+ const filteredRoutes = {};
1450
+ for (const route of routes) {
1451
+ if (Object.prototype.hasOwnProperty.call(data.routes, route)) {
1452
+ filteredRoutes[route] = data.routes[route];
1453
+ }
1454
+ }
1455
+ return {
1456
+ routes: filteredRoutes
1457
+ };
1458
+ }
1043
1459
  async function promptAnswers() {
1044
1460
  const defaults = getDefaultAnswers();
1045
1461
  const rl = createInterface({ input, output });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schemasentry/cli",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for Schema Sentry validation and reporting.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "commander": "^12.0.0",
36
- "@schemasentry/core": "0.3.2"
36
+ "@schemasentry/core": "0.4.0"
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsup src/index.ts --format esm --dts --clean --tsconfig tsconfig.build.json",