@emeryld/rrroutes-export 1.1.1 → 1.1.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.
Files changed (49) hide show
  1. package/README.md +115 -1
  2. package/bin/rrroutes-checkpoint.mjs +9 -0
  3. package/bin/rrroutes-export-changes.mjs +9 -0
  4. package/bin/rrroutes-export-snapshot.mjs +9 -0
  5. package/bin/rrroutes-inspector-serve.mjs +9 -0
  6. package/dist/adapters/express.d.ts +6 -0
  7. package/dist/checkpoint/lock.d.ts +4 -0
  8. package/dist/checkpoint/retention.d.ts +15 -0
  9. package/dist/checkpoint/service.d.ts +11 -0
  10. package/dist/checkpoint/types.d.ts +65 -0
  11. package/dist/cli/changes.d.ts +4 -0
  12. package/dist/cli/checkpoint.d.ts +1 -0
  13. package/dist/cli/runtime.d.ts +7 -0
  14. package/dist/cli/serve.d.ts +7 -0
  15. package/dist/cli/snapshot.d.ts +4 -0
  16. package/dist/diff/diff-snapshots.d.ts +4 -0
  17. package/dist/diff/types.d.ts +58 -0
  18. package/dist/exportFinalizedLeaves.changelog.d.ts +4 -0
  19. package/dist/files/baked-payload.d.ts +21 -0
  20. package/dist/files/write.d.ts +10 -0
  21. package/dist/http/handler.d.ts +2 -0
  22. package/dist/http/types.d.ts +24 -0
  23. package/dist/index.cjs +3094 -118
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.ts +25 -0
  26. package/dist/index.mjs +3065 -116
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/inspection/diagnostics.d.ts +8 -0
  29. package/dist/inspection/inspect-clients.d.ts +9 -0
  30. package/dist/inspection/inspect-contract.d.ts +8 -0
  31. package/dist/inspection/inspect-server.d.ts +8 -0
  32. package/dist/inspection/inspect-sockets.d.ts +4 -0
  33. package/dist/inspection/join-routes.d.ts +10 -0
  34. package/dist/inspection/native.d.ts +4 -0
  35. package/dist/inspection/normalize-clients.d.ts +8 -0
  36. package/dist/inspection/normalize-routes.d.ts +3 -0
  37. package/dist/inspection/normalize-sockets.d.ts +7 -0
  38. package/dist/inspection/serializable.d.ts +3 -0
  39. package/dist/runtime.d.ts +16 -0
  40. package/dist/serializeLeafContract.d.ts +1 -0
  41. package/dist/snapshot/build.d.ts +2 -0
  42. package/dist/snapshot/canonicalize.d.ts +6 -0
  43. package/dist/snapshot/hash.d.ts +5 -0
  44. package/dist/snapshot/types.d.ts +287 -0
  45. package/dist/storage/filesystem.d.ts +5 -0
  46. package/dist/storage/memory.d.ts +2 -0
  47. package/dist/storage/sqlite.d.ts +15 -0
  48. package/dist/storage/types.d.ts +27 -0
  49. package/package.json +66 -7
package/dist/index.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __typeError = (msg) => {
9
+ throw TypeError(msg);
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -26,33 +29,66 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
29
  mod
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
33
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
34
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
29
35
 
30
36
  // src/index.ts
31
37
  var index_exports = {};
32
38
  __export(index_exports, {
33
39
  DEFAULT_VIEWER_TEMPLATE: () => DEFAULT_VIEWER_TEMPLATE,
40
+ RRROUTES_BAKED_PAYLOAD_MARKER: () => RRROUTES_BAKED_PAYLOAD_MARKER,
34
41
  __private: () => __private,
42
+ applyChangeSetRetention: () => applyChangeSetRetention,
43
+ applyCheckpointRetention: () => applyCheckpointRetention,
35
44
  buildFinalizedLeavesViewerBundle: () => buildFinalizedLeavesViewerBundle,
45
+ buildRRRoutesSnapshot: () => buildRRRoutesSnapshot,
46
+ canonicalSnapshotStructure: () => canonicalSnapshotStructure,
47
+ canonicalizeJson: () => canonicalizeJson,
36
48
  clearSchemaIntrospectionHandlers: () => clearSchemaIntrospectionHandlers,
49
+ createBakedInspectorPayload: () => createBakedInspectorPayload,
50
+ createCheckpointService: () => createCheckpointService,
51
+ createExpressRouter: () => createExpressRouter,
52
+ createFileSystemInspectionStorage: () => createFileSystemInspectionStorage,
53
+ createInspectorHttpHandler: () => createInspectorHttpHandler,
54
+ createMemoryInspectionStorage: () => createMemoryInspectionStorage,
55
+ createRRRoutesExportRuntime: () => createRRRoutesExportRuntime,
56
+ createSQLiteInspectionStorage: () => createSQLiteInspectionStorage,
37
57
  createSchemaIntrospector: () => createSchemaIntrospector,
58
+ createSnapshotChangeSet: () => createSnapshotChangeSet,
59
+ diffRRRoutesSnapshots: () => diffRRRoutesSnapshots,
60
+ escapeBakedPayload: () => escapeBakedPayload,
38
61
  exportFinalizedLeaves: () => exportFinalizedLeaves,
39
62
  exportFinalizedLeavesChangelog: () => exportFinalizedLeavesChangelog,
40
63
  extractLeafSourceByAst: () => extractLeafSourceByAst,
41
64
  flattenLeafSchemas: () => flattenLeafSchemas,
42
65
  flattenSerializableSchema: () => flattenSerializableSchema,
66
+ hashCanonicalValue: () => hashCanonicalValue,
67
+ hashRRRoutesSnapshot: () => hashRRRoutesSnapshot,
68
+ injectBakedInspectorPayload: () => injectBakedInspectorPayload,
43
69
  introspectSchema: () => introspectSchema,
70
+ loadExportRuntime: () => loadExportRuntime,
44
71
  loadFinalizedLeavesInput: () => loadFinalizedLeavesInput,
72
+ parseCliFlags: () => parseCliFlags,
45
73
  parseFinalizedLeavesChangelogCliArgs: () => parseFinalizedLeavesChangelogCliArgs,
46
74
  parseFinalizedLeavesCliArgs: () => parseFinalizedLeavesCliArgs,
47
75
  registerSchemaIntrospectionHandler: () => registerSchemaIntrospectionHandler,
76
+ requiredFlag: () => requiredFlag,
77
+ runChangesCli: () => runChangesCli,
78
+ runCheckpointCli: () => runCheckpointCli,
48
79
  runExportFinalizedLeavesChangelogCli: () => runExportFinalizedLeavesChangelogCli,
49
80
  runExportFinalizedLeavesCli: () => runExportFinalizedLeavesCli,
81
+ runInspectorServeCli: () => runInspectorServeCli,
82
+ runSnapshotCli: () => runSnapshotCli,
50
83
  serializableSchemaKinds: () => serializableSchemaKinds,
51
84
  serializeLeafContract: () => serializeLeafContract,
52
85
  serializeLeavesContract: () => serializeLeavesContract,
86
+ stableStringify: () => stableStringify,
53
87
  writeFinalizedLeavesChangelogExport: () => writeFinalizedLeavesChangelogExport,
54
88
  writeFinalizedLeavesExport: () => writeFinalizedLeavesExport,
55
- writeFinalizedLeavesViewerBundle: () => writeFinalizedLeavesViewerBundle
89
+ writeFinalizedLeavesViewerBundle: () => writeFinalizedLeavesViewerBundle,
90
+ writeRRRoutesChangeSet: () => writeRRRoutesChangeSet,
91
+ writeRRRoutesSnapshot: () => writeRRRoutesSnapshot
56
92
  });
57
93
  module.exports = __toCommonJS(index_exports);
58
94
 
@@ -283,6 +319,7 @@ function serializeLeafContract(leaf, options = {}) {
283
319
  const cfg = leaf.cfg;
284
320
  return {
285
321
  key: `${leaf.method.toUpperCase()} ${leaf.path}`,
322
+ id: cfg.id,
286
323
  method: leaf.method,
287
324
  path: leaf.path,
288
325
  cfg: {
@@ -355,46 +392,46 @@ function normalizeType(schema) {
355
392
  return "unknown";
356
393
  }
357
394
  }
358
- function isNonEmptyPath(path7) {
359
- return typeof path7 === "string" && path7.length > 0;
395
+ function isNonEmptyPath(path12) {
396
+ return typeof path12 === "string" && path12.length > 0;
360
397
  }
361
- function setNode(out, path7, schema, inherited) {
362
- if (!isNonEmptyPath(path7)) return;
363
- out[path7] = {
398
+ function setNode(out, path12, schema, inherited) {
399
+ if (!isNonEmptyPath(path12)) return;
400
+ out[path12] = {
364
401
  type: normalizeType(schema),
365
402
  nullable: inherited.nullable || Boolean(schema.nullable),
366
403
  optional: inherited.optional || Boolean(schema.optional),
367
404
  literal: schema.kind === "literal" ? schema.literal : void 0
368
405
  };
369
406
  }
370
- function flattenInto(out, schema, path7, inherited) {
371
- if (!schema || !isNonEmptyPath(path7)) return;
407
+ function flattenInto(out, schema, path12, inherited) {
408
+ if (!schema || !isNonEmptyPath(path12)) return;
372
409
  const nextInherited = {
373
410
  optional: inherited.optional || Boolean(schema.optional),
374
411
  nullable: inherited.nullable || Boolean(schema.nullable)
375
412
  };
376
413
  if (schema.kind === "union" && Array.isArray(schema.union) && schema.union.length > 0) {
377
414
  schema.union.forEach((option, index) => {
378
- const optionPath = `${path7}-${index + 1}`;
415
+ const optionPath = `${path12}-${index + 1}`;
379
416
  flattenInto(out, option, optionPath, inherited);
380
417
  });
381
418
  return;
382
419
  }
383
- setNode(out, path7, schema, inherited);
420
+ setNode(out, path12, schema, inherited);
384
421
  if (schema.kind === "object" && schema.properties) {
385
422
  for (const [key, child] of Object.entries(schema.properties)) {
386
- const childPath = `${path7}.${key}`;
423
+ const childPath = `${path12}.${key}`;
387
424
  flattenInto(out, child, childPath, nextInherited);
388
425
  }
389
426
  return;
390
427
  }
391
428
  if (schema.kind === "array" && schema.element) {
392
- flattenInto(out, schema.element, `${path7}[]`, nextInherited);
429
+ flattenInto(out, schema.element, `${path12}[]`, nextInherited);
393
430
  }
394
431
  }
395
- function flattenSerializableSchema(schema, path7) {
432
+ function flattenSerializableSchema(schema, path12) {
396
433
  const out = {};
397
- flattenInto(out, schema, path7, { optional: false, nullable: false });
434
+ flattenInto(out, schema, path12, { optional: false, nullable: false });
398
435
  return out;
399
436
  }
400
437
  function isSerializedLeaf(value) {
@@ -1118,6 +1155,33 @@ function extractLeafSourceByAst({
1118
1155
  };
1119
1156
  }
1120
1157
 
1158
+ // src/files/baked-payload.ts
1159
+ var RRROUTES_BAKED_PAYLOAD_MARKER = "<!--__RRROUTES_INSPECTOR_BAKED_PAYLOAD__-->";
1160
+ function createBakedInspectorPayload(args) {
1161
+ return {
1162
+ kind: "rrroutes-inspector-baked",
1163
+ snapshot: args.snapshot,
1164
+ checkpoints: args.checkpoints ?? [],
1165
+ changeSets: args.changeSets ?? []
1166
+ };
1167
+ }
1168
+ function escapeBakedPayload(payload) {
1169
+ return JSON.stringify(payload).replace(/<\//g, "<\\/").replace(/<!--/g, "<\\!--").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1170
+ }
1171
+ function injectBakedInspectorPayload(html, payload, options = {}) {
1172
+ const marker = options.marker ?? RRROUTES_BAKED_PAYLOAD_MARKER;
1173
+ const globalName = options.globalName ?? "__RRROUTES_INSPECTOR_PAYLOAD";
1174
+ const scriptId = options.scriptId ?? "rrroutes-inspector-baked-payload";
1175
+ const script = `${marker}
1176
+ <script id="${scriptId}">window.${globalName} = ${escapeBakedPayload(payload)};</script>`;
1177
+ if (html.includes(marker)) return html.replace(marker, script);
1178
+ if (html.includes("</body>"))
1179
+ return html.replace("</body>", `${script}
1180
+ </body>`);
1181
+ return `${script}
1182
+ ${html}`;
1183
+ }
1184
+
1121
1185
  // src/exportFinalizedLeaves.ts
1122
1186
  function isRegistry(value) {
1123
1187
  return typeof value === "object" && value !== null && "all" in value && "byKey" in value;
@@ -1131,7 +1195,7 @@ function buildMeta(sourceExtraction) {
1131
1195
  description: "Finalized RRRoutes leaves export with serialized schemas and flattened schema paths for downstream processing.",
1132
1196
  sourceExtraction,
1133
1197
  fieldCatalog: {
1134
- leaf: ["key", "method", "path", "cfg"],
1198
+ leaf: ["key", "id?", "method", "path", "cfg"],
1135
1199
  cfg: [
1136
1200
  "description?",
1137
1201
  "summary?",
@@ -1165,25 +1229,6 @@ function buildMeta(sourceExtraction) {
1165
1229
  }
1166
1230
  };
1167
1231
  }
1168
- var BAKED_PAYLOAD_MARKER = "<!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->";
1169
- function escapePayloadForInlineScript(payload) {
1170
- return JSON.stringify(payload).replace(/<\//g, "<\\/").replace(/<!--/g, "<\\!--");
1171
- }
1172
- function injectPayloadIntoViewerHtml(htmlTemplate, payload) {
1173
- const payloadScript = `${BAKED_PAYLOAD_MARKER}
1174
- <script id="finalized-leaves-baked-payload">window.__FINALIZED_LEAVES_PAYLOAD = ${escapePayloadForInlineScript(
1175
- payload
1176
- )};</script>`;
1177
- if (htmlTemplate.includes(BAKED_PAYLOAD_MARKER)) {
1178
- return htmlTemplate.replace(BAKED_PAYLOAD_MARKER, payloadScript);
1179
- }
1180
- if (htmlTemplate.includes("</body>")) {
1181
- return htmlTemplate.replace("</body>", `${payloadScript}
1182
- </body>`);
1183
- }
1184
- return `${payloadScript}
1185
- ${htmlTemplate}`;
1186
- }
1187
1232
  async function resolveViewerTemplatePath(viewerTemplateFile) {
1188
1233
  if (viewerTemplateFile) {
1189
1234
  const resolved = import_node_path2.default.resolve(viewerTemplateFile);
@@ -1195,10 +1240,7 @@ async function resolveViewerTemplatePath(viewerTemplateFile) {
1195
1240
  process.cwd(),
1196
1241
  "node_modules/@emeryld/rrroutes-export/tools/finalized-leaves-viewer.html"
1197
1242
  ),
1198
- import_node_path2.default.resolve(
1199
- process.cwd(),
1200
- "tools/finalized-leaves-viewer.html"
1201
- ),
1243
+ import_node_path2.default.resolve(process.cwd(), "tools/finalized-leaves-viewer.html"),
1202
1244
  import_node_path2.default.resolve(
1203
1245
  process.cwd(),
1204
1246
  "packages/export/tools/finalized-leaves-viewer.html"
@@ -1223,7 +1265,11 @@ async function writeJsonExport(payload, outFile) {
1223
1265
  async function writeBakedHtmlExport(payload, htmlFile, viewerTemplateFile) {
1224
1266
  const templatePath = await resolveViewerTemplatePath(viewerTemplateFile);
1225
1267
  const template = templatePath ? await import_promises.default.readFile(templatePath, "utf8") : DEFAULT_VIEWER_TEMPLATE;
1226
- const baked = injectPayloadIntoViewerHtml(template, payload);
1268
+ const baked = injectBakedInspectorPayload(template, payload, {
1269
+ marker: "<!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->",
1270
+ globalName: "__FINALIZED_LEAVES_PAYLOAD",
1271
+ scriptId: "finalized-leaves-baked-payload"
1272
+ });
1227
1273
  const resolved = import_node_path2.default.resolve(htmlFile);
1228
1274
  await import_promises.default.mkdir(import_node_path2.default.dirname(resolved), { recursive: true });
1229
1275
  await import_promises.default.writeFile(resolved, baked, "utf8");
@@ -1399,11 +1445,874 @@ async function runExportFinalizedLeavesCli(argv) {
1399
1445
  var import_promises2 = __toESM(require("fs/promises"), 1);
1400
1446
  var import_node_os = __toESM(require("os"), 1);
1401
1447
  var import_node_path4 = __toESM(require("path"), 1);
1402
- var import_node_crypto = __toESM(require("crypto"), 1);
1448
+ var import_node_crypto2 = __toESM(require("crypto"), 1);
1403
1449
  var import_node_child_process2 = require("child_process");
1404
1450
  var import_node_util = require("util");
1405
1451
  var import_node_url2 = require("url");
1406
1452
  var import_typescript2 = __toESM(require("typescript"), 1);
1453
+
1454
+ // src/snapshot/canonicalize.ts
1455
+ function compareJson(left, right) {
1456
+ return JSON.stringify(left).localeCompare(JSON.stringify(right));
1457
+ }
1458
+ function canonicalizeValue(value, key) {
1459
+ if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
1460
+ return void 0;
1461
+ }
1462
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
1463
+ return value;
1464
+ }
1465
+ if (typeof value === "number") {
1466
+ return Number.isFinite(value) ? value : null;
1467
+ }
1468
+ if (typeof value === "bigint") return String(value);
1469
+ if (value instanceof Date) return value.toISOString();
1470
+ if (Array.isArray(value)) {
1471
+ const array = value.map((item) => canonicalizeValue(item)).filter((item) => item !== void 0);
1472
+ if (key === "tags" || key === "events" || key === "eventNames") {
1473
+ return array.slice().sort(compareJson);
1474
+ }
1475
+ return array;
1476
+ }
1477
+ if (typeof value !== "object") return void 0;
1478
+ const out = {};
1479
+ for (const property of Object.keys(value).sort()) {
1480
+ const child = canonicalizeValue(
1481
+ value[property],
1482
+ property
1483
+ );
1484
+ if (child !== void 0) out[property] = child;
1485
+ }
1486
+ return out;
1487
+ }
1488
+ function canonicalizeJson(value) {
1489
+ return canonicalizeValue(value) ?? null;
1490
+ }
1491
+ function canonicalSnapshotStructure(snapshot, options = {}) {
1492
+ const sockets = Object.fromEntries(
1493
+ Object.entries(snapshot.sockets).map(([identity, socket]) => [
1494
+ identity,
1495
+ options.includeLive ? socket : { ...socket, live: void 0 }
1496
+ ])
1497
+ );
1498
+ return canonicalizeJson({
1499
+ ...snapshot,
1500
+ meta: {
1501
+ ...snapshot.meta,
1502
+ capturedAt: void 0,
1503
+ snapshotHash: void 0
1504
+ },
1505
+ capabilities: {
1506
+ ...snapshot.capabilities,
1507
+ live: false
1508
+ },
1509
+ sockets
1510
+ });
1511
+ }
1512
+ function stableStringify(value) {
1513
+ return JSON.stringify(canonicalizeJson(value));
1514
+ }
1515
+
1516
+ // src/snapshot/hash.ts
1517
+ var import_node_crypto = require("crypto");
1518
+ function hashCanonicalValue(value) {
1519
+ return (0, import_node_crypto.createHash)("sha256").update(stableStringify(value)).digest("hex");
1520
+ }
1521
+ function hashRRRoutesSnapshot(snapshot, options = {}) {
1522
+ return hashCanonicalValue(canonicalSnapshotStructure(snapshot, options));
1523
+ }
1524
+
1525
+ // src/diff/diff-snapshots.ts
1526
+ function same(left, right) {
1527
+ return stableStringify(left) === stableStringify(right);
1528
+ }
1529
+ function severityFor(type) {
1530
+ switch (type) {
1531
+ case "route_removed":
1532
+ case "client_binding_removed":
1533
+ case "client_resource_removed":
1534
+ case "server_controller_unregistered":
1535
+ case "server_endpoint_disabled":
1536
+ case "socket_runtime_removed":
1537
+ case "socket_contract_event_removed":
1538
+ return "breaking";
1539
+ case "schema_changed":
1540
+ case "cfg_changed":
1541
+ case "client_binding_target_changed":
1542
+ case "client_build_options_changed":
1543
+ case "client_augment_changed":
1544
+ case "client_augment_reordered":
1545
+ case "client_socket_connection_changed":
1546
+ case "server_controller_changed":
1547
+ case "server_middleware_changed":
1548
+ case "server_middleware_reordered":
1549
+ case "server_output_validation_changed":
1550
+ case "socket_payload_schema_changed":
1551
+ case "socket_binding_changed":
1552
+ case "socket_room_strategy_changed":
1553
+ case "socket_cache_reducer_changed":
1554
+ case "socket_heartbeat_configuration_changed":
1555
+ return "behavioral";
1556
+ case "route_added":
1557
+ case "client_resource_added":
1558
+ case "client_binding_added":
1559
+ case "client_binding_enabled":
1560
+ case "client_augment_added":
1561
+ case "client_socket_connection_added":
1562
+ case "server_controller_registered":
1563
+ case "server_endpoint_enabled":
1564
+ case "server_middleware_added":
1565
+ case "socket_runtime_added":
1566
+ case "socket_contract_event_added":
1567
+ case "socket_handler_added":
1568
+ return "non-breaking";
1569
+ case "route_id_changed":
1570
+ case "description_changed":
1571
+ case "files_changed":
1572
+ case "source_changed":
1573
+ case "client_binding_disabled":
1574
+ case "client_augment_removed":
1575
+ case "client_socket_connection_removed":
1576
+ case "server_middleware_removed":
1577
+ case "socket_handler_removed":
1578
+ return "informational";
1579
+ }
1580
+ }
1581
+ function makeChange(input) {
1582
+ const before = input.before === void 0 ? void 0 : canonicalizeJson(input.before);
1583
+ const after = input.after === void 0 ? void 0 : canonicalizeJson(input.after);
1584
+ const base = {
1585
+ ...input,
1586
+ before,
1587
+ after,
1588
+ severity: input.severity ?? severityFor(input.type)
1589
+ };
1590
+ return {
1591
+ ...base,
1592
+ id: hashCanonicalValue(base)
1593
+ };
1594
+ }
1595
+ function fieldChanges(before, after, fields) {
1596
+ return fields.flatMap(
1597
+ (field) => same(before[field], after[field]) ? [] : [
1598
+ {
1599
+ field,
1600
+ before: canonicalizeJson(before[field]),
1601
+ after: canonicalizeJson(after[field])
1602
+ }
1603
+ ]
1604
+ );
1605
+ }
1606
+ function diffContract(before, after) {
1607
+ const changes = [];
1608
+ const keys = /* @__PURE__ */ new Set([
1609
+ ...Object.keys(before.routes),
1610
+ ...Object.keys(after.routes)
1611
+ ]);
1612
+ for (const routeKey of Array.from(keys).sort()) {
1613
+ const previous = before.routes[routeKey];
1614
+ const next = after.routes[routeKey];
1615
+ if (!previous && next) {
1616
+ changes.push(
1617
+ makeChange({
1618
+ layer: "contract",
1619
+ type: "route_added",
1620
+ routeKey,
1621
+ entityId: routeKey,
1622
+ after: next.contract.serializedLeaf
1623
+ })
1624
+ );
1625
+ continue;
1626
+ }
1627
+ if (previous && !next) {
1628
+ changes.push(
1629
+ makeChange({
1630
+ layer: "contract",
1631
+ type: "route_removed",
1632
+ routeKey,
1633
+ entityId: routeKey,
1634
+ before: previous.contract.serializedLeaf
1635
+ })
1636
+ );
1637
+ continue;
1638
+ }
1639
+ if (!previous || !next) continue;
1640
+ if (previous.id !== next.id) {
1641
+ changes.push(
1642
+ makeChange({
1643
+ layer: "contract",
1644
+ type: "route_id_changed",
1645
+ routeKey,
1646
+ entityId: routeKey,
1647
+ before: previous.id,
1648
+ after: next.id
1649
+ })
1650
+ );
1651
+ }
1652
+ if (!same(previous.contract.schemaFlat, next.contract.schemaFlat)) {
1653
+ changes.push(
1654
+ makeChange({
1655
+ layer: "contract",
1656
+ type: "schema_changed",
1657
+ routeKey,
1658
+ entityId: routeKey,
1659
+ before: previous.contract.schemaFlat,
1660
+ after: next.contract.schemaFlat
1661
+ })
1662
+ );
1663
+ }
1664
+ const previousCfg = previous.contract.serializedLeaf.cfg;
1665
+ const nextCfg = next.contract.serializedLeaf.cfg;
1666
+ if (previousCfg.description !== nextCfg.description) {
1667
+ changes.push(
1668
+ makeChange({
1669
+ layer: "contract",
1670
+ type: "description_changed",
1671
+ routeKey,
1672
+ entityId: routeKey,
1673
+ before: previousCfg.description,
1674
+ after: nextCfg.description
1675
+ })
1676
+ );
1677
+ }
1678
+ if (!same(previousCfg.bodyFiles, nextCfg.bodyFiles)) {
1679
+ changes.push(
1680
+ makeChange({
1681
+ layer: "contract",
1682
+ type: "files_changed",
1683
+ routeKey,
1684
+ entityId: routeKey,
1685
+ before: previousCfg.bodyFiles,
1686
+ after: nextCfg.bodyFiles
1687
+ })
1688
+ );
1689
+ }
1690
+ const cfgFields = [
1691
+ "summary",
1692
+ "docsGroup",
1693
+ "tags",
1694
+ "deprecated",
1695
+ "stability",
1696
+ "docsHidden",
1697
+ "docsMeta",
1698
+ "feed"
1699
+ ];
1700
+ const fields = fieldChanges(
1701
+ previousCfg,
1702
+ nextCfg,
1703
+ cfgFields
1704
+ );
1705
+ if (fields.length > 0) {
1706
+ changes.push(
1707
+ makeChange({
1708
+ layer: "contract",
1709
+ type: "cfg_changed",
1710
+ routeKey,
1711
+ entityId: routeKey,
1712
+ before: previousCfg,
1713
+ after: nextCfg,
1714
+ changedFields: fields
1715
+ })
1716
+ );
1717
+ }
1718
+ if (!same(previous.contract.source, next.contract.source)) {
1719
+ changes.push(
1720
+ makeChange({
1721
+ layer: "contract",
1722
+ type: "source_changed",
1723
+ routeKey,
1724
+ entityId: routeKey,
1725
+ before: previous.contract.source,
1726
+ after: next.contract.source
1727
+ })
1728
+ );
1729
+ }
1730
+ }
1731
+ return changes;
1732
+ }
1733
+ function diffOrderedEntities(args) {
1734
+ const changes = [];
1735
+ const previous = new Map(args.before.map((item) => [item.identity, item]));
1736
+ const next = new Map(args.after.map((item) => [item.identity, item]));
1737
+ const identities = /* @__PURE__ */ new Set([...previous.keys(), ...next.keys()]);
1738
+ for (const identity of Array.from(identities).sort()) {
1739
+ const left = previous.get(identity);
1740
+ const right = next.get(identity);
1741
+ if (!left && right) {
1742
+ changes.push(
1743
+ makeChange({
1744
+ layer: args.layer,
1745
+ type: args.added,
1746
+ routeKey: args.routeKey,
1747
+ entityId: identity,
1748
+ after: right
1749
+ })
1750
+ );
1751
+ } else if (left && !right) {
1752
+ changes.push(
1753
+ makeChange({
1754
+ layer: args.layer,
1755
+ type: args.removed,
1756
+ routeKey: args.routeKey,
1757
+ entityId: identity,
1758
+ before: left
1759
+ })
1760
+ );
1761
+ } else if (left && right) {
1762
+ const leftWithoutOrder = { ...left, order: void 0 };
1763
+ const rightWithoutOrder = { ...right, order: void 0 };
1764
+ if (!same(leftWithoutOrder, rightWithoutOrder)) {
1765
+ changes.push(
1766
+ makeChange({
1767
+ layer: args.layer,
1768
+ type: args.changed,
1769
+ routeKey: args.routeKey,
1770
+ entityId: identity,
1771
+ before: left,
1772
+ after: right
1773
+ })
1774
+ );
1775
+ } else if (left.order !== right.order) {
1776
+ changes.push(
1777
+ makeChange({
1778
+ layer: args.layer,
1779
+ type: args.reordered,
1780
+ routeKey: args.routeKey,
1781
+ entityId: identity,
1782
+ before: left.order,
1783
+ after: right.order
1784
+ })
1785
+ );
1786
+ }
1787
+ }
1788
+ }
1789
+ return changes;
1790
+ }
1791
+ function allBindings(snapshot) {
1792
+ const map = /* @__PURE__ */ new Map();
1793
+ for (const client of Object.values(snapshot.clients)) {
1794
+ for (const binding of client.bindings)
1795
+ map.set(binding.bindingIdentity, binding);
1796
+ }
1797
+ return map;
1798
+ }
1799
+ function diffClientSocketConnections(args) {
1800
+ const changes = [];
1801
+ const previous = new Map(args.before.map((item) => [item.identity, item]));
1802
+ const next = new Map(args.after.map((item) => [item.identity, item]));
1803
+ const identities = /* @__PURE__ */ new Set([...previous.keys(), ...next.keys()]);
1804
+ for (const identity of Array.from(identities).sort()) {
1805
+ const left = previous.get(identity);
1806
+ const right = next.get(identity);
1807
+ if (!left && right) {
1808
+ changes.push(
1809
+ makeChange({
1810
+ layer: "client",
1811
+ type: "client_socket_connection_added",
1812
+ routeKey: args.routeKey,
1813
+ entityId: identity,
1814
+ after: right
1815
+ })
1816
+ );
1817
+ continue;
1818
+ }
1819
+ if (left && !right) {
1820
+ changes.push(
1821
+ makeChange({
1822
+ layer: "client",
1823
+ type: "client_socket_connection_removed",
1824
+ routeKey: args.routeKey,
1825
+ entityId: identity,
1826
+ before: left
1827
+ })
1828
+ );
1829
+ continue;
1830
+ }
1831
+ if (!left || !right) continue;
1832
+ const leftConnection = {
1833
+ id: left.id,
1834
+ name: left.name,
1835
+ events: left.events,
1836
+ order: left.order,
1837
+ identityStability: left.identityStability
1838
+ };
1839
+ const rightConnection = {
1840
+ id: right.id,
1841
+ name: right.name,
1842
+ events: right.events,
1843
+ order: right.order,
1844
+ identityStability: right.identityStability
1845
+ };
1846
+ if (!same(leftConnection, rightConnection)) {
1847
+ changes.push(
1848
+ makeChange({
1849
+ layer: "client",
1850
+ type: "client_socket_connection_changed",
1851
+ routeKey: args.routeKey,
1852
+ entityId: identity,
1853
+ before: leftConnection,
1854
+ after: rightConnection
1855
+ })
1856
+ );
1857
+ }
1858
+ if (!same(left.roomStrategy, right.roomStrategy)) {
1859
+ changes.push(
1860
+ makeChange({
1861
+ layer: "socket",
1862
+ type: "socket_room_strategy_changed",
1863
+ routeKey: args.routeKey,
1864
+ entityId: identity,
1865
+ before: left.roomStrategy,
1866
+ after: right.roomStrategy
1867
+ })
1868
+ );
1869
+ }
1870
+ if (!same(left.cacheReducers, right.cacheReducers)) {
1871
+ changes.push(
1872
+ makeChange({
1873
+ layer: "socket",
1874
+ type: "socket_cache_reducer_changed",
1875
+ routeKey: args.routeKey,
1876
+ entityId: identity,
1877
+ before: left.cacheReducers,
1878
+ after: right.cacheReducers
1879
+ })
1880
+ );
1881
+ }
1882
+ }
1883
+ return changes;
1884
+ }
1885
+ function diffClients(before, after) {
1886
+ const changes = [];
1887
+ const resourceIds = /* @__PURE__ */ new Set([
1888
+ ...Object.keys(before.clients),
1889
+ ...Object.keys(after.clients)
1890
+ ]);
1891
+ for (const identity of Array.from(resourceIds).sort()) {
1892
+ if (!before.clients[identity] && after.clients[identity]) {
1893
+ changes.push(
1894
+ makeChange({
1895
+ layer: "client",
1896
+ type: "client_resource_added",
1897
+ entityId: identity,
1898
+ after: after.clients[identity]
1899
+ })
1900
+ );
1901
+ } else if (before.clients[identity] && !after.clients[identity]) {
1902
+ changes.push(
1903
+ makeChange({
1904
+ layer: "client",
1905
+ type: "client_resource_removed",
1906
+ entityId: identity,
1907
+ before: before.clients[identity]
1908
+ })
1909
+ );
1910
+ }
1911
+ }
1912
+ const previous = allBindings(before);
1913
+ const next = allBindings(after);
1914
+ const identities = /* @__PURE__ */ new Set([...previous.keys(), ...next.keys()]);
1915
+ for (const identity of Array.from(identities).sort()) {
1916
+ const left = previous.get(identity);
1917
+ const right = next.get(identity);
1918
+ if (!left && right) {
1919
+ changes.push(
1920
+ makeChange({
1921
+ layer: "client",
1922
+ type: "client_binding_added",
1923
+ routeKey: right.routeKey,
1924
+ entityId: identity,
1925
+ after: right
1926
+ })
1927
+ );
1928
+ continue;
1929
+ }
1930
+ if (left && !right) {
1931
+ changes.push(
1932
+ makeChange({
1933
+ layer: "client",
1934
+ type: "client_binding_removed",
1935
+ routeKey: left.routeKey,
1936
+ entityId: identity,
1937
+ before: left
1938
+ })
1939
+ );
1940
+ continue;
1941
+ }
1942
+ if (!left || !right) continue;
1943
+ if (left.routeKey !== right.routeKey) {
1944
+ changes.push(
1945
+ makeChange({
1946
+ layer: "client",
1947
+ type: "client_binding_target_changed",
1948
+ routeKey: right.routeKey,
1949
+ entityId: identity,
1950
+ before: left.routeKey,
1951
+ after: right.routeKey
1952
+ })
1953
+ );
1954
+ }
1955
+ if (left.enabled !== right.enabled) {
1956
+ changes.push(
1957
+ makeChange({
1958
+ layer: "client",
1959
+ type: right.enabled ? "client_binding_enabled" : "client_binding_disabled",
1960
+ routeKey: right.routeKey,
1961
+ entityId: identity,
1962
+ before: left.enabled,
1963
+ after: right.enabled
1964
+ })
1965
+ );
1966
+ }
1967
+ if (!same(left.build, right.build)) {
1968
+ changes.push(
1969
+ makeChange({
1970
+ layer: "client",
1971
+ type: "client_build_options_changed",
1972
+ routeKey: right.routeKey,
1973
+ entityId: identity,
1974
+ before: left.build,
1975
+ after: right.build
1976
+ })
1977
+ );
1978
+ }
1979
+ changes.push(
1980
+ ...diffOrderedEntities({
1981
+ before: left.augments,
1982
+ after: right.augments,
1983
+ layer: "client",
1984
+ routeKey: right.routeKey,
1985
+ added: "client_augment_added",
1986
+ removed: "client_augment_removed",
1987
+ changed: "client_augment_changed",
1988
+ reordered: "client_augment_reordered"
1989
+ })
1990
+ );
1991
+ changes.push(
1992
+ ...diffClientSocketConnections({
1993
+ before: left.socketConnections,
1994
+ after: right.socketConnections,
1995
+ routeKey: right.routeKey
1996
+ })
1997
+ );
1998
+ }
1999
+ return changes;
2000
+ }
2001
+ function routeServerMap(snapshot) {
2002
+ const map = /* @__PURE__ */ new Map();
2003
+ for (const [routeKey, route] of Object.entries(snapshot.routes)) {
2004
+ if (route.server) map.set(routeKey, route.server);
2005
+ }
2006
+ return map;
2007
+ }
2008
+ function diffServers(before, after) {
2009
+ const changes = [];
2010
+ const previous = routeServerMap(before);
2011
+ const next = routeServerMap(after);
2012
+ const keys = /* @__PURE__ */ new Set([...previous.keys(), ...next.keys()]);
2013
+ for (const routeKey of Array.from(keys).sort()) {
2014
+ const left = previous.get(routeKey);
2015
+ const right = next.get(routeKey);
2016
+ if ((!left || !left.registered) && right?.registered) {
2017
+ changes.push(
2018
+ makeChange({
2019
+ layer: "server",
2020
+ type: "server_controller_registered",
2021
+ routeKey,
2022
+ entityId: right.controllerId ?? routeKey,
2023
+ after: right
2024
+ })
2025
+ );
2026
+ continue;
2027
+ }
2028
+ if (left?.registered && (!right || !right.registered)) {
2029
+ changes.push(
2030
+ makeChange({
2031
+ layer: "server",
2032
+ type: "server_controller_unregistered",
2033
+ routeKey,
2034
+ entityId: left.controllerId ?? routeKey,
2035
+ before: left
2036
+ })
2037
+ );
2038
+ continue;
2039
+ }
2040
+ if (!left || !right || !left.registered || !right.registered) continue;
2041
+ if (left.enabled !== right.enabled) {
2042
+ changes.push(
2043
+ makeChange({
2044
+ layer: "server",
2045
+ type: right.enabled ? "server_endpoint_enabled" : "server_endpoint_disabled",
2046
+ routeKey,
2047
+ entityId: right.controllerId ?? routeKey,
2048
+ before: left.enabled,
2049
+ after: right.enabled
2050
+ })
2051
+ );
2052
+ }
2053
+ if (left.controllerId !== right.controllerId || left.controllerName !== right.controllerName) {
2054
+ changes.push(
2055
+ makeChange({
2056
+ layer: "server",
2057
+ type: "server_controller_changed",
2058
+ routeKey,
2059
+ entityId: right.controllerId ?? routeKey,
2060
+ before: {
2061
+ id: left.controllerId ?? null,
2062
+ name: left.controllerName ?? null
2063
+ },
2064
+ after: {
2065
+ id: right.controllerId ?? null,
2066
+ name: right.controllerName ?? null
2067
+ }
2068
+ })
2069
+ );
2070
+ }
2071
+ changes.push(
2072
+ ...diffOrderedEntities({
2073
+ before: left.before,
2074
+ after: right.before,
2075
+ layer: "server",
2076
+ routeKey,
2077
+ added: "server_middleware_added",
2078
+ removed: "server_middleware_removed",
2079
+ changed: "server_middleware_changed",
2080
+ reordered: "server_middleware_reordered"
2081
+ })
2082
+ );
2083
+ if (left.validateOutput !== right.validateOutput) {
2084
+ changes.push(
2085
+ makeChange({
2086
+ layer: "server",
2087
+ type: "server_output_validation_changed",
2088
+ routeKey,
2089
+ entityId: right.controllerId ?? routeKey,
2090
+ before: left.validateOutput,
2091
+ after: right.validateOutput
2092
+ })
2093
+ );
2094
+ }
2095
+ }
2096
+ return changes;
2097
+ }
2098
+ function diffSockets(before, after) {
2099
+ const changes = [];
2100
+ const identities = /* @__PURE__ */ new Set([
2101
+ ...Object.keys(before.sockets),
2102
+ ...Object.keys(after.sockets)
2103
+ ]);
2104
+ for (const identity of Array.from(identities).sort()) {
2105
+ const left = before.sockets[identity];
2106
+ const right = after.sockets[identity];
2107
+ if (!left && right) {
2108
+ changes.push(
2109
+ makeChange({
2110
+ layer: "socket",
2111
+ type: "socket_runtime_added",
2112
+ entityId: identity,
2113
+ after: right
2114
+ })
2115
+ );
2116
+ continue;
2117
+ }
2118
+ if (left && !right) {
2119
+ changes.push(
2120
+ makeChange({
2121
+ layer: "socket",
2122
+ type: "socket_runtime_removed",
2123
+ entityId: identity,
2124
+ before: left
2125
+ })
2126
+ );
2127
+ continue;
2128
+ }
2129
+ if (!left || !right) continue;
2130
+ const eventNames = /* @__PURE__ */ new Set([
2131
+ ...Object.keys(left.contract.events),
2132
+ ...Object.keys(right.contract.events)
2133
+ ]);
2134
+ for (const eventName of Array.from(eventNames).sort()) {
2135
+ const previousEvent = left.contract.events[eventName];
2136
+ const nextEvent = right.contract.events[eventName];
2137
+ const eventIdentity = `${identity}:event:${eventName}`;
2138
+ if (!previousEvent && nextEvent) {
2139
+ changes.push(
2140
+ makeChange({
2141
+ layer: "socket",
2142
+ type: "socket_contract_event_added",
2143
+ entityId: eventIdentity,
2144
+ after: nextEvent
2145
+ })
2146
+ );
2147
+ } else if (previousEvent && !nextEvent) {
2148
+ changes.push(
2149
+ makeChange({
2150
+ layer: "socket",
2151
+ type: "socket_contract_event_removed",
2152
+ entityId: eventIdentity,
2153
+ before: previousEvent
2154
+ })
2155
+ );
2156
+ } else if (previousEvent && nextEvent && !same(previousEvent.schema, nextEvent.schema)) {
2157
+ changes.push(
2158
+ makeChange({
2159
+ layer: "socket",
2160
+ type: "socket_payload_schema_changed",
2161
+ entityId: eventIdentity,
2162
+ before: previousEvent.schema,
2163
+ after: nextEvent.schema
2164
+ })
2165
+ );
2166
+ }
2167
+ }
2168
+ if (!same(left.configuration.heartbeat, right.configuration.heartbeat)) {
2169
+ changes.push(
2170
+ makeChange({
2171
+ layer: "socket",
2172
+ type: "socket_heartbeat_configuration_changed",
2173
+ entityId: identity,
2174
+ before: left.configuration.heartbeat,
2175
+ after: right.configuration.heartbeat
2176
+ })
2177
+ );
2178
+ }
2179
+ const previousHandlers = new Map(
2180
+ left.configuration.handlers.map((handler) => [handler.identity, handler])
2181
+ );
2182
+ const nextHandlers = new Map(
2183
+ right.configuration.handlers.map((handler) => [handler.identity, handler])
2184
+ );
2185
+ for (const handlerIdentity of Array.from(
2186
+ /* @__PURE__ */ new Set([...previousHandlers.keys(), ...nextHandlers.keys()])
2187
+ ).sort()) {
2188
+ const previousHandler = previousHandlers.get(handlerIdentity);
2189
+ const nextHandler = nextHandlers.get(handlerIdentity);
2190
+ if (!previousHandler && nextHandler) {
2191
+ changes.push(
2192
+ makeChange({
2193
+ layer: "socket",
2194
+ type: "socket_handler_added",
2195
+ entityId: handlerIdentity,
2196
+ after: nextHandler
2197
+ })
2198
+ );
2199
+ } else if (previousHandler && !nextHandler) {
2200
+ changes.push(
2201
+ makeChange({
2202
+ layer: "socket",
2203
+ type: "socket_handler_removed",
2204
+ entityId: handlerIdentity,
2205
+ before: previousHandler
2206
+ })
2207
+ );
2208
+ } else if (previousHandler && nextHandler && !same(previousHandler, nextHandler)) {
2209
+ changes.push(
2210
+ makeChange({
2211
+ layer: "socket",
2212
+ type: "socket_handler_removed",
2213
+ entityId: handlerIdentity,
2214
+ before: previousHandler
2215
+ }),
2216
+ makeChange({
2217
+ layer: "socket",
2218
+ type: "socket_handler_added",
2219
+ entityId: handlerIdentity,
2220
+ after: nextHandler
2221
+ })
2222
+ );
2223
+ }
2224
+ }
2225
+ const previousBindings = new Map(
2226
+ left.bindings.map((binding) => [binding.identity, binding])
2227
+ );
2228
+ const nextBindings = new Map(
2229
+ right.bindings.map((binding) => [binding.identity, binding])
2230
+ );
2231
+ for (const bindingIdentity of Array.from(
2232
+ /* @__PURE__ */ new Set([...previousBindings.keys(), ...nextBindings.keys()])
2233
+ ).sort()) {
2234
+ const previousBinding = previousBindings.get(bindingIdentity);
2235
+ const nextBinding = nextBindings.get(bindingIdentity);
2236
+ if (!same(previousBinding, nextBinding)) {
2237
+ changes.push(
2238
+ makeChange({
2239
+ layer: "socket",
2240
+ type: "socket_binding_changed",
2241
+ routeKey: nextBinding?.routeKey ?? previousBinding?.routeKey,
2242
+ entityId: bindingIdentity,
2243
+ before: previousBinding,
2244
+ after: nextBinding
2245
+ })
2246
+ );
2247
+ }
2248
+ }
2249
+ }
2250
+ return changes;
2251
+ }
2252
+ function summarize(changes) {
2253
+ const byLayer = {
2254
+ contract: 0,
2255
+ client: 0,
2256
+ server: 0,
2257
+ socket: 0
2258
+ };
2259
+ const bySeverity = {
2260
+ breaking: 0,
2261
+ behavioral: 0,
2262
+ "non-breaking": 0,
2263
+ informational: 0
2264
+ };
2265
+ const byType = {};
2266
+ for (const change of changes) {
2267
+ byLayer[change.layer] += 1;
2268
+ bySeverity[change.severity] += 1;
2269
+ byType[change.type] = (byType[change.type] ?? 0) + 1;
2270
+ }
2271
+ return { total: changes.length, byLayer, bySeverity, byType };
2272
+ }
2273
+ function diffRRRoutesSnapshots(before, after, options = {}) {
2274
+ const changes = [
2275
+ ...diffContract(before, after),
2276
+ ...diffClients(before, after),
2277
+ ...diffServers(before, after),
2278
+ ...diffSockets(before, after)
2279
+ ].sort((left, right) => {
2280
+ const route = (left.routeKey ?? "").localeCompare(right.routeKey ?? "");
2281
+ if (route !== 0) return route;
2282
+ const layer = left.layer.localeCompare(right.layer);
2283
+ if (layer !== 0) return layer;
2284
+ const type = left.type.localeCompare(right.type);
2285
+ return type !== 0 ? type : left.entityId.localeCompare(right.entityId);
2286
+ });
2287
+ const scope = options.scope ?? "default";
2288
+ const trigger = options.trigger ?? { type: "manual" };
2289
+ const transition = {
2290
+ scope,
2291
+ from: before.meta.snapshotHash,
2292
+ to: after.meta.snapshotHash
2293
+ };
2294
+ return {
2295
+ id: hashCanonicalValue(transition),
2296
+ scope,
2297
+ createdAt: options.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2298
+ from: {
2299
+ snapshotHash: before.meta.snapshotHash,
2300
+ capturedAt: before.meta.capturedAt,
2301
+ checkpointId: options.fromCheckpointId
2302
+ },
2303
+ to: {
2304
+ snapshotHash: after.meta.snapshotHash,
2305
+ capturedAt: after.meta.capturedAt,
2306
+ checkpointId: options.toCheckpointId
2307
+ },
2308
+ trigger,
2309
+ summary: summarize(changes),
2310
+ changes
2311
+ };
2312
+ }
2313
+ var createSnapshotChangeSet = diffRRRoutesSnapshots;
2314
+
2315
+ // src/exportFinalizedLeaves.changelog.ts
1407
2316
  var execFile = (0, import_node_util.promisify)(import_node_child_process2.execFile);
1408
2317
  var CHANGESET_CFG_FIELDS = [
1409
2318
  "summary",
@@ -1416,7 +2325,15 @@ var CHANGESET_CFG_FIELDS = [
1416
2325
  "feed"
1417
2326
  ];
1418
2327
  var SCHEMA_SECTIONS = ["params", "query", "body", "output"];
1419
- var MODULE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs"];
2328
+ var MODULE_EXTENSIONS = [
2329
+ ".ts",
2330
+ ".tsx",
2331
+ ".mts",
2332
+ ".cts",
2333
+ ".js",
2334
+ ".mjs",
2335
+ ".cjs"
2336
+ ];
1420
2337
  var COMPILE_CACHE_VERSION = "v2";
1421
2338
  var SOURCE_KEY_BY_SECTION = {
1422
2339
  params: "paramsSchema",
@@ -1427,7 +2344,7 @@ var SOURCE_KEY_BY_SECTION = {
1427
2344
  function normalizePath(p) {
1428
2345
  return p.replace(/\\/g, "/");
1429
2346
  }
1430
- function stableStringify(value) {
2347
+ function stableStringify2(value) {
1431
2348
  return JSON.stringify(value, (_key, v) => {
1432
2349
  if (v && typeof v === "object" && !Array.isArray(v)) {
1433
2350
  const sorted = Object.entries(v).sort(([a], [b]) => a.localeCompare(b)).reduce((acc, [k, inner]) => {
@@ -1456,7 +2373,7 @@ function toCfgSubset(cfg) {
1456
2373
  return out;
1457
2374
  }
1458
2375
  function entrySignature(entry) {
1459
- return stableStringify({
2376
+ return stableStringify2({
1460
2377
  type: entry.type,
1461
2378
  nullable: Boolean(entry.nullable),
1462
2379
  optional: Boolean(entry.optional),
@@ -1464,7 +2381,9 @@ function entrySignature(entry) {
1464
2381
  });
1465
2382
  }
1466
2383
  function semverToTuple(tag) {
1467
- const match = tag.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
2384
+ const match = tag.match(
2385
+ /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
2386
+ );
1468
2387
  if (!match) return null;
1469
2388
  return {
1470
2389
  major: Number(match[1]),
@@ -1510,7 +2429,9 @@ async function findDefaultFromCommit(cwd, toCommit) {
1510
2429
  const root = await runGit(cwd, ["rev-list", "--max-parents=0", toCommit]);
1511
2430
  const rootCommit = root.split("\n").map((item) => item.trim()).filter(Boolean)[0];
1512
2431
  if (!rootCommit) {
1513
- throw new Error("Failed to resolve a baseline commit for changelog generation.");
2432
+ throw new Error(
2433
+ "Failed to resolve a baseline commit for changelog generation."
2434
+ );
1514
2435
  }
1515
2436
  return rootCommit;
1516
2437
  }
@@ -1629,11 +2550,15 @@ async function commitTouchedRelevantPaths(cwd, commit, moduleCandidates, tsconfi
1629
2550
  ]);
1630
2551
  const changedFiles = changed.split("\n").map((item) => normalizePath(item.trim()).replace(/^\/+/, "")).filter(Boolean);
1631
2552
  const moduleFiles = Array.from(
1632
- new Set(moduleCandidates.map((item) => normalizePath(item).replace(/^\/+/, "")))
2553
+ new Set(
2554
+ moduleCandidates.map((item) => normalizePath(item).replace(/^\/+/, ""))
2555
+ )
1633
2556
  );
1634
2557
  const moduleDirs = Array.from(
1635
2558
  new Set(
1636
- moduleFiles.map((item) => normalizePath(import_node_path4.default.posix.dirname(item)).replace(/^\/+/, "")).filter((item) => item && item !== ".")
2559
+ moduleFiles.map(
2560
+ (item) => normalizePath(import_node_path4.default.posix.dirname(item)).replace(/^\/+/, "")
2561
+ ).filter((item) => item && item !== ".")
1637
2562
  )
1638
2563
  );
1639
2564
  const tsconfigFile = tsconfigRel ? normalizePath(tsconfigRel).replace(/^\/+/, "") : void 0;
@@ -1656,7 +2581,12 @@ async function filterCommits(cwd, commits, moduleCandidates, tsconfigRel) {
1656
2581
  const keep = [commits[0]];
1657
2582
  for (let i = 1; i < commits.length; i += 1) {
1658
2583
  const commit = commits[i];
1659
- if (await commitTouchedRelevantPaths(cwd, commit, moduleCandidates, tsconfigRel)) {
2584
+ if (await commitTouchedRelevantPaths(
2585
+ cwd,
2586
+ commit,
2587
+ moduleCandidates,
2588
+ tsconfigRel
2589
+ )) {
1660
2590
  keep.push(commit);
1661
2591
  }
1662
2592
  }
@@ -1667,7 +2597,12 @@ async function filterCommits(cwd, commits, moduleCandidates, tsconfigRel) {
1667
2597
  return keep;
1668
2598
  }
1669
2599
  async function getCommitMetadata(cwd, commit) {
1670
- const out = await runGit(cwd, ["show", "-s", "--format=%H%x1f%aI%x1f%an%x1f%s", commit]);
2600
+ const out = await runGit(cwd, [
2601
+ "show",
2602
+ "-s",
2603
+ "--format=%H%x1f%aI%x1f%an%x1f%s",
2604
+ commit
2605
+ ]);
1671
2606
  const [sha, authorDate, authorName, subject] = out.split("");
1672
2607
  if (!sha || !authorDate || !authorName) {
1673
2608
  throw new Error(`Unable to read commit metadata for ${commit}`);
@@ -1810,7 +2745,7 @@ function diffRouteCfg(before, after) {
1810
2745
  for (const field of CHANGESET_CFG_FIELDS) {
1811
2746
  const prev = before[field];
1812
2747
  const next = after[field];
1813
- if (stableStringify(prev) !== stableStringify(next)) {
2748
+ if (stableStringify2(prev) !== stableStringify2(next)) {
1814
2749
  out.push({
1815
2750
  field,
1816
2751
  before: prev,
@@ -1872,7 +2807,7 @@ function diffSourceSchemas(before, after) {
1872
2807
  continue;
1873
2808
  }
1874
2809
  if (!prevSignatures || !nextSignatures) continue;
1875
- if (stableStringify(prevSignatures) !== stableStringify(nextSignatures)) {
2810
+ if (stableStringify2(prevSignatures) !== stableStringify2(nextSignatures)) {
1876
2811
  out.push({
1877
2812
  section,
1878
2813
  path: schemaPath,
@@ -2013,7 +2948,7 @@ function toEmittedModulePath(moduleRel) {
2013
2948
  return normalized;
2014
2949
  }
2015
2950
  function buildCompileCacheKey(commitSha, moduleRel, tsconfigKeyPath) {
2016
- return import_node_crypto.default.createHash("sha1").update(COMPILE_CACHE_VERSION).update("\n").update(commitSha).update("\n").update(normalizePath(moduleRel)).update("\n").update(normalizePath(tsconfigKeyPath)).digest("hex");
2951
+ return import_node_crypto2.default.createHash("sha1").update(COMPILE_CACHE_VERSION).update("\n").update(commitSha).update("\n").update(normalizePath(moduleRel)).update("\n").update(normalizePath(tsconfigKeyPath)).digest("hex");
2017
2952
  }
2018
2953
  async function collectFilesRecursive(rootDir) {
2019
2954
  const files = [];
@@ -2086,13 +3021,16 @@ async function compileSnapshotEntry(options) {
2086
3021
  log(
2087
3022
  `[changelog] compile cache ${options.noCache ? "disabled" : "miss"}: ${options.commitSha.slice(0, 12)}`
2088
3023
  );
2089
- await import_promises2.default.rm(import_node_path4.default.join(options.cacheRoot, cacheKey), { recursive: true, force: true }).catch(
2090
- () => void 0
2091
- );
3024
+ await import_promises2.default.rm(import_node_path4.default.join(options.cacheRoot, cacheKey), {
3025
+ recursive: true,
3026
+ force: true
3027
+ }).catch(() => void 0);
2092
3028
  await import_promises2.default.mkdir(emitRoot, { recursive: true });
2093
3029
  const readResult = import_typescript2.default.readConfigFile(options.tsconfigPath, import_typescript2.default.sys.readFile);
2094
3030
  if (readResult.error) {
2095
- throw new Error(import_typescript2.default.flattenDiagnosticMessageText(readResult.error.messageText, "\n"));
3031
+ throw new Error(
3032
+ import_typescript2.default.flattenDiagnosticMessageText(readResult.error.messageText, "\n")
3033
+ );
2096
3034
  }
2097
3035
  const parsed = import_typescript2.default.parseJsonConfigFileContent(
2098
3036
  readResult.config,
@@ -2121,7 +3059,10 @@ async function compileSnapshotEntry(options) {
2121
3059
  getCurrentDirectory: () => options.worktreeDir,
2122
3060
  getNewLine: () => "\n"
2123
3061
  };
2124
- const message = import_typescript2.default.formatDiagnosticsWithColorAndContext(diagnostics.slice(0, 20), host);
3062
+ const message = import_typescript2.default.formatDiagnosticsWithColorAndContext(
3063
+ diagnostics.slice(0, 20),
3064
+ host
3065
+ );
2125
3066
  throw new Error(
2126
3067
  `TypeScript snapshot compile failed for ${options.moduleRel} at ${options.commitSha.slice(0, 12)}:
2127
3068
  ${message}`
@@ -2141,7 +3082,13 @@ async function loadSnapshotExport(options) {
2141
3082
  }
2142
3083
  async function createSnapshot(repoRoot, moduleCandidates, exportName, commit, tempRoot, tsconfigRel, exportOptions, log) {
2143
3084
  const worktreeDir = import_node_path4.default.join(tempRoot, `wt-${commit.sha.slice(0, 12)}`);
2144
- await runGit(repoRoot, ["worktree", "add", "--detach", worktreeDir, commit.sha]);
3085
+ await runGit(repoRoot, [
3086
+ "worktree",
3087
+ "add",
3088
+ "--detach",
3089
+ worktreeDir,
3090
+ commit.sha
3091
+ ]);
2145
3092
  try {
2146
3093
  try {
2147
3094
  await ensureWorktreeDependencies(repoRoot, worktreeDir);
@@ -2226,13 +3173,94 @@ async function createSnapshot(repoRoot, moduleCandidates, exportName, commit, te
2226
3173
  };
2227
3174
  }
2228
3175
  } finally {
2229
- await runGit(repoRoot, ["worktree", "remove", "--force", worktreeDir]).catch(() => void 0);
3176
+ await runGit(repoRoot, [
3177
+ "worktree",
3178
+ "remove",
3179
+ "--force",
3180
+ worktreeDir
3181
+ ]).catch(() => void 0);
2230
3182
  await import_promises2.default.rm(worktreeDir, { recursive: true, force: true }).catch(() => void 0);
2231
3183
  }
2232
3184
  }
3185
+ function toCanonicalGitSnapshot(snapshot) {
3186
+ const routes = Object.fromEntries(
3187
+ Object.entries(snapshot.routesByKey).map(([routeKey, route]) => [
3188
+ routeKey,
3189
+ {
3190
+ routeKey,
3191
+ method: route.method,
3192
+ path: route.path,
3193
+ contract: {
3194
+ serializedLeaf: {
3195
+ key: routeKey,
3196
+ method: route.method.toLowerCase(),
3197
+ path: route.path,
3198
+ cfg: { ...route.cfg, schemas: {} }
3199
+ },
3200
+ schemaFlat: route.flatSchema,
3201
+ source: route.sourceBySection,
3202
+ fingerprint: hashCanonicalValue({
3203
+ cfg: route.cfg,
3204
+ schema: route.flatSchema
3205
+ })
3206
+ },
3207
+ clients: [],
3208
+ sockets: [],
3209
+ status: {
3210
+ client: "unbound",
3211
+ server: "unavailable",
3212
+ socket: "none"
3213
+ }
3214
+ }
3215
+ ])
3216
+ );
3217
+ const snapshotHash = hashCanonicalValue(routes);
3218
+ return {
3219
+ schemaVersion: 1,
3220
+ meta: {
3221
+ capturedAt: snapshot.commit.authorDate,
3222
+ snapshotHash,
3223
+ commit: snapshot.commit.sha,
3224
+ packageVersions: {}
3225
+ },
3226
+ capabilities: {
3227
+ contract: true,
3228
+ client: false,
3229
+ server: false,
3230
+ socket: false,
3231
+ live: false,
3232
+ source: true
3233
+ },
3234
+ routes,
3235
+ clients: {},
3236
+ servers: {},
3237
+ sockets: {},
3238
+ diagnostics: []
3239
+ };
3240
+ }
2233
3241
  function diffSnapshots(previous, current) {
2234
3242
  const routeEvents = [];
2235
3243
  const sourceEvents = [];
3244
+ const changeSet = diffRRRoutesSnapshots(
3245
+ toCanonicalGitSnapshot(previous),
3246
+ toCanonicalGitSnapshot(current),
3247
+ {
3248
+ scope: "git",
3249
+ trigger: {
3250
+ type: "git",
3251
+ externalId: current.commit.sha,
3252
+ label: current.commit.subject
3253
+ },
3254
+ createdAt: current.commit.authorDate
3255
+ }
3256
+ );
3257
+ const canonicalRouteTypes = /* @__PURE__ */ new Map();
3258
+ for (const change of changeSet.changes) {
3259
+ if (change.layer !== "contract" || !change.routeKey) continue;
3260
+ const types = canonicalRouteTypes.get(change.routeKey) ?? /* @__PURE__ */ new Set();
3261
+ types.add(change.type);
3262
+ canonicalRouteTypes.set(change.routeKey, types);
3263
+ }
2236
3264
  const routeKeys = /* @__PURE__ */ new Set([
2237
3265
  ...Object.keys(previous.routesByKey),
2238
3266
  ...Object.keys(current.routesByKey)
@@ -2240,7 +3268,8 @@ function diffSnapshots(previous, current) {
2240
3268
  for (const routeKey of Array.from(routeKeys).sort()) {
2241
3269
  const prevRoute = previous.routesByKey[routeKey];
2242
3270
  const nextRoute = current.routesByKey[routeKey];
2243
- if (!prevRoute && nextRoute) {
3271
+ const canonicalTypes = canonicalRouteTypes.get(routeKey) ?? /* @__PURE__ */ new Set();
3272
+ if (!prevRoute && nextRoute && canonicalTypes.has("route_added")) {
2244
3273
  const schemaDiff2 = diffRouteSchemas({}, nextRoute.flatSchema);
2245
3274
  routeEvents.push({
2246
3275
  type: "route_added",
@@ -2252,7 +3281,7 @@ function diffSnapshots(previous, current) {
2252
3281
  });
2253
3282
  continue;
2254
3283
  }
2255
- if (prevRoute && !nextRoute) {
3284
+ if (prevRoute && !nextRoute && canonicalTypes.has("route_removed")) {
2256
3285
  routeEvents.push({
2257
3286
  type: "route_removed",
2258
3287
  commitSha: current.commit.sha,
@@ -2263,8 +3292,11 @@ function diffSnapshots(previous, current) {
2263
3292
  continue;
2264
3293
  }
2265
3294
  if (!prevRoute || !nextRoute) continue;
2266
- const schemaDiff = diffRouteSchemas(prevRoute.flatSchema, nextRoute.flatSchema);
2267
- if (schemaDiff.length > 0) {
3295
+ const schemaDiff = diffRouteSchemas(
3296
+ prevRoute.flatSchema,
3297
+ nextRoute.flatSchema
3298
+ );
3299
+ if (schemaDiff.length > 0 && canonicalTypes.has("schema_changed")) {
2268
3300
  routeEvents.push({
2269
3301
  type: "schema_changed",
2270
3302
  commitSha: current.commit.sha,
@@ -2275,7 +3307,9 @@ function diffSnapshots(previous, current) {
2275
3307
  });
2276
3308
  }
2277
3309
  const cfgDiff = diffRouteCfg(prevRoute.cfg, nextRoute.cfg);
2278
- if (cfgDiff.length > 0) {
3310
+ if (cfgDiff.length > 0 && ["cfg_changed", "description_changed", "files_changed"].some(
3311
+ (type) => canonicalTypes.has(type)
3312
+ )) {
2279
3313
  routeEvents.push({
2280
3314
  type: "cfg_changed",
2281
3315
  commitSha: current.commit.sha,
@@ -2322,8 +3356,12 @@ function diffSnapshots(previous, current) {
2322
3356
  const schemaDiff = diffSourceSchemas(prevSource, nextSource);
2323
3357
  const prevRoutes = new Set(prevSource.routeKeys);
2324
3358
  const nextRoutes = new Set(nextSource.routeKeys);
2325
- const routeAdded = Array.from(nextRoutes).filter((routeKey) => !prevRoutes.has(routeKey));
2326
- const routeRemoved = Array.from(prevRoutes).filter((routeKey) => !nextRoutes.has(routeKey));
3359
+ const routeAdded = Array.from(nextRoutes).filter(
3360
+ (routeKey) => !prevRoutes.has(routeKey)
3361
+ );
3362
+ const routeRemoved = Array.from(prevRoutes).filter(
3363
+ (routeKey) => !nextRoutes.has(routeKey)
3364
+ );
2327
3365
  if (schemaDiff.length > 0 || routeAdded.length > 0 || routeRemoved.length > 0) {
2328
3366
  sourceEvents.push({
2329
3367
  type: "source_schema_changed",
@@ -2339,28 +3377,10 @@ function diffSnapshots(previous, current) {
2339
3377
  }
2340
3378
  return {
2341
3379
  routeEvents,
2342
- sourceEvents
3380
+ sourceEvents,
3381
+ changeSet
2343
3382
  };
2344
3383
  }
2345
- var BAKED_PAYLOAD_MARKER2 = "<!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->";
2346
- function escapePayloadForInlineScript2(payload) {
2347
- return JSON.stringify(payload).replace(/<\//g, "<\\/").replace(/<!--/g, "<\\!--");
2348
- }
2349
- function injectPayloadIntoViewerHtml2(htmlTemplate, payload) {
2350
- const payloadScript = `${BAKED_PAYLOAD_MARKER2}
2351
- <script id="finalized-leaves-baked-payload">window.__FINALIZED_LEAVES_PAYLOAD = ${escapePayloadForInlineScript2(
2352
- payload
2353
- )};</script>`;
2354
- if (htmlTemplate.includes(BAKED_PAYLOAD_MARKER2)) {
2355
- return htmlTemplate.replace(BAKED_PAYLOAD_MARKER2, payloadScript);
2356
- }
2357
- if (htmlTemplate.includes("</body>")) {
2358
- return htmlTemplate.replace("</body>", `${payloadScript}
2359
- </body>`);
2360
- }
2361
- return `${payloadScript}
2362
- ${htmlTemplate}`;
2363
- }
2364
3384
  async function resolveViewerTemplatePath2(viewerTemplateFile) {
2365
3385
  if (viewerTemplateFile) {
2366
3386
  const resolved = import_node_path4.default.resolve(viewerTemplateFile);
@@ -2373,7 +3393,10 @@ async function resolveViewerTemplatePath2(viewerTemplateFile) {
2373
3393
  "node_modules/@emeryld/rrroutes-export/tools/finalized-leaves-viewer.html"
2374
3394
  ),
2375
3395
  import_node_path4.default.resolve(process.cwd(), "tools/finalized-leaves-viewer.html"),
2376
- import_node_path4.default.resolve(process.cwd(), "packages/export/tools/finalized-leaves-viewer.html")
3396
+ import_node_path4.default.resolve(
3397
+ process.cwd(),
3398
+ "packages/export/tools/finalized-leaves-viewer.html"
3399
+ )
2377
3400
  ];
2378
3401
  for (const candidate of candidates) {
2379
3402
  try {
@@ -2394,7 +3417,11 @@ async function writeJsonExport2(payload, outFile) {
2394
3417
  async function writeBakedHtmlExport2(payload, htmlFile, viewerTemplateFile) {
2395
3418
  const templatePath = await resolveViewerTemplatePath2(viewerTemplateFile);
2396
3419
  const template = templatePath ? await import_promises2.default.readFile(templatePath, "utf8") : DEFAULT_VIEWER_TEMPLATE;
2397
- const baked = injectPayloadIntoViewerHtml2(template, payload);
3420
+ const baked = injectBakedInspectorPayload(template, payload, {
3421
+ marker: "<!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->",
3422
+ globalName: "__FINALIZED_LEAVES_PAYLOAD",
3423
+ scriptId: "finalized-leaves-baked-payload"
3424
+ });
2398
3425
  const resolved = import_node_path4.default.resolve(htmlFile);
2399
3426
  await import_promises2.default.mkdir(import_node_path4.default.dirname(resolved), { recursive: true });
2400
3427
  await import_promises2.default.writeFile(resolved, baked, "utf8");
@@ -2445,7 +3472,9 @@ async function exportFinalizedLeavesChangelog(options) {
2445
3472
  log(`[changelog] resolving repository root from ${cwd}`);
2446
3473
  const repoRoot = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
2447
3474
  const modulePathAbsolute = import_node_path4.default.resolve(cwd, options.modulePath);
2448
- const modulePathRelative = normalizePath(import_node_path4.default.relative(repoRoot, modulePathAbsolute));
3475
+ const modulePathRelative = normalizePath(
3476
+ import_node_path4.default.relative(repoRoot, modulePathAbsolute)
3477
+ );
2449
3478
  if (modulePathRelative.startsWith("..")) {
2450
3479
  throw new Error("modulePath must resolve inside the git repository root.");
2451
3480
  }
@@ -2457,7 +3486,9 @@ async function exportFinalizedLeavesChangelog(options) {
2457
3486
  const exportName = options.exportName ?? "leaves";
2458
3487
  log("[changelog] resolving commit window");
2459
3488
  const window = await resolveCommitWindow(repoRoot, options.from, options.to);
2460
- log(`[changelog] window: ${window.from.slice(0, 12)}..${window.to.slice(0, 12)}`);
3489
+ log(
3490
+ `[changelog] window: ${window.from.slice(0, 12)}..${window.to.slice(0, 12)}`
3491
+ );
2461
3492
  const moduleCandidates = await buildModulePathCandidates(
2462
3493
  repoRoot,
2463
3494
  window.to,
@@ -2482,7 +3513,9 @@ async function exportFinalizedLeavesChangelog(options) {
2482
3513
  for (const commit of selectedCommitShas) {
2483
3514
  commits.push(await getCommitMetadata(repoRoot, commit));
2484
3515
  }
2485
- const tempRoot = await import_promises2.default.mkdtemp(import_node_path4.default.join(import_node_os.default.tmpdir(), "rrroutes-export-changelog-"));
3516
+ const tempRoot = await import_promises2.default.mkdtemp(
3517
+ import_node_path4.default.join(import_node_os.default.tmpdir(), "rrroutes-export-changelog-")
3518
+ );
2486
3519
  log(`[changelog] temp workspace: ${tempRoot}`);
2487
3520
  try {
2488
3521
  const snapshots = [];
@@ -2519,6 +3552,7 @@ async function exportFinalizedLeavesChangelog(options) {
2519
3552
  }
2520
3553
  const routeEvents = [];
2521
3554
  const sourceEvents = [];
3555
+ const changeSets = [];
2522
3556
  log("[changelog] diffing successful snapshots");
2523
3557
  let previousSnapshot;
2524
3558
  for (const snapshot of snapshots) {
@@ -2529,12 +3563,14 @@ async function exportFinalizedLeavesChangelog(options) {
2529
3563
  previousSnapshot = snapshot;
2530
3564
  continue;
2531
3565
  }
2532
- const { routeEvents: nextRouteEvents, sourceEvents: nextSourceEvents } = diffSnapshots(
2533
- previousSnapshot,
2534
- snapshot
2535
- );
3566
+ const {
3567
+ routeEvents: nextRouteEvents,
3568
+ sourceEvents: nextSourceEvents,
3569
+ changeSet
3570
+ } = diffSnapshots(previousSnapshot, snapshot);
2536
3571
  routeEvents.push(...nextRouteEvents);
2537
3572
  sourceEvents.push(...nextSourceEvents);
3573
+ changeSets.push(changeSet);
2538
3574
  previousSnapshot = snapshot;
2539
3575
  }
2540
3576
  log(
@@ -2556,7 +3592,8 @@ async function exportFinalizedLeavesChangelog(options) {
2556
3592
  commits,
2557
3593
  byRoute: routeRecordByKey(routeEvents),
2558
3594
  bySourceObject: sourceRecordById(sourceEvents),
2559
- rollups: buildRollups(routeEvents, sourceEvents, commits)
3595
+ rollups: buildRollups(routeEvents, sourceEvents, commits),
3596
+ changeSets
2560
3597
  };
2561
3598
  if (options.outFile || options.htmlFile) {
2562
3599
  log("[changelog] writing output artifacts");
@@ -2663,25 +3700,6 @@ async function runExportFinalizedLeavesChangelogCli(argv) {
2663
3700
  var import_promises3 = __toESM(require("fs/promises"), 1);
2664
3701
  var import_node_path6 = __toESM(require("path"), 1);
2665
3702
  var import_node_child_process3 = require("child_process");
2666
- var BAKED_PAYLOAD_MARKER3 = "<!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->";
2667
- function escapePayloadForInlineScript3(payload) {
2668
- return JSON.stringify(payload).replace(/<\//g, "<\\/").replace(/<!--/g, "<\\!--");
2669
- }
2670
- function injectPayloadIntoViewerHtml3(htmlTemplate, payload) {
2671
- const payloadScript = `${BAKED_PAYLOAD_MARKER3}
2672
- <script id="finalized-leaves-baked-payload">window.__FINALIZED_LEAVES_PAYLOAD = ${escapePayloadForInlineScript3(
2673
- payload
2674
- )};</script>`;
2675
- if (htmlTemplate.includes(BAKED_PAYLOAD_MARKER3)) {
2676
- return htmlTemplate.replace(BAKED_PAYLOAD_MARKER3, payloadScript);
2677
- }
2678
- if (htmlTemplate.includes("</body>")) {
2679
- return htmlTemplate.replace("</body>", `${payloadScript}
2680
- </body>`);
2681
- }
2682
- return `${payloadScript}
2683
- ${htmlTemplate}`;
2684
- }
2685
3703
  async function resolveViewerTemplatePath3(viewerTemplateFile) {
2686
3704
  if (viewerTemplateFile) {
2687
3705
  const resolved = import_node_path6.default.resolve(viewerTemplateFile);
@@ -2694,7 +3712,10 @@ async function resolveViewerTemplatePath3(viewerTemplateFile) {
2694
3712
  "node_modules/@emeryld/rrroutes-export/tools/finalized-leaves-viewer.html"
2695
3713
  ),
2696
3714
  import_node_path6.default.resolve(process.cwd(), "tools/finalized-leaves-viewer.html"),
2697
- import_node_path6.default.resolve(process.cwd(), "packages/export/tools/finalized-leaves-viewer.html")
3715
+ import_node_path6.default.resolve(
3716
+ process.cwd(),
3717
+ "packages/export/tools/finalized-leaves-viewer.html"
3718
+ )
2698
3719
  ];
2699
3720
  for (const candidate of candidates) {
2700
3721
  try {
@@ -2715,7 +3736,11 @@ async function writeJsonExport3(payload, outFile) {
2715
3736
  async function writeBakedHtmlExport3(payload, htmlFile, viewerTemplateFile) {
2716
3737
  const templatePath = await resolveViewerTemplatePath3(viewerTemplateFile);
2717
3738
  const template = templatePath ? await import_promises3.default.readFile(templatePath, "utf8") : DEFAULT_VIEWER_TEMPLATE;
2718
- const baked = injectPayloadIntoViewerHtml3(template, payload);
3739
+ const baked = injectBakedInspectorPayload(template, payload, {
3740
+ marker: "<!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->",
3741
+ globalName: "__FINALIZED_LEAVES_PAYLOAD",
3742
+ scriptId: "finalized-leaves-baked-payload"
3743
+ });
2719
3744
  const resolved = import_node_path6.default.resolve(htmlFile);
2720
3745
  await import_promises3.default.mkdir(import_node_path6.default.dirname(resolved), { recursive: true });
2721
3746
  await import_promises3.default.writeFile(resolved, baked, "utf8");
@@ -2771,30 +3796,1981 @@ async function writeFinalizedLeavesViewerBundle(payload, outFileOrOptions) {
2771
3796
  }
2772
3797
  return written;
2773
3798
  }
3799
+
3800
+ // src/snapshot/build.ts
3801
+ var import_rrroutes_contract5 = require("@emeryld/rrroutes-contract");
3802
+
3803
+ // src/inspection/diagnostics.ts
3804
+ var _items;
3805
+ var InspectionDiagnostics = class {
3806
+ constructor() {
3807
+ __privateAdd(this, _items, []);
3808
+ }
3809
+ add(diagnostic) {
3810
+ __privateGet(this, _items).push(diagnostic);
3811
+ }
3812
+ list(options = {}) {
3813
+ const items = options.includeInfo === false ? __privateGet(this, _items).filter((item) => item.severity !== "info") : __privateGet(this, _items);
3814
+ return items.slice().sort((left, right) => {
3815
+ const byCode = left.code.localeCompare(right.code);
3816
+ if (byCode !== 0) return byCode;
3817
+ return JSON.stringify(left.entity ?? {}).localeCompare(
3818
+ JSON.stringify(right.entity ?? {})
3819
+ );
3820
+ });
3821
+ }
3822
+ };
3823
+ _items = new WeakMap();
3824
+
3825
+ // src/inspection/inspect-clients.ts
3826
+ var import_rrroutes_contract3 = require("@emeryld/rrroutes-contract");
3827
+
3828
+ // src/inspection/native.ts
3829
+ var import_rrroutes_contract2 = require("@emeryld/rrroutes-contract");
3830
+ function asRecord2(value) {
3831
+ return typeof value === "object" && value !== null ? value : void 0;
3832
+ }
3833
+ function readNativeInspection(value) {
3834
+ const record = asRecord2(value);
3835
+ if (!record) return void 0;
3836
+ const inspect = record[import_rrroutes_contract2.RRROUTES_INSPECTION];
3837
+ if (typeof inspect !== "function") return void 0;
3838
+ try {
3839
+ return asRecord2(inspect.call(value));
3840
+ } catch {
3841
+ return void 0;
3842
+ }
3843
+ }
3844
+ function optionalString(value) {
3845
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3846
+ }
3847
+ function optionalBoolean(value) {
3848
+ return typeof value === "boolean" ? value : void 0;
3849
+ }
3850
+
3851
+ // src/inspection/normalize-clients.ts
3852
+ function normalizeClientsInput(endpointClient, allClients) {
3853
+ const out = [];
3854
+ if (endpointClient) {
3855
+ out.push({
3856
+ value: endpointClient,
3857
+ registeredName: "endpointClient",
3858
+ index: 0
3859
+ });
3860
+ }
3861
+ if (!allClients) return out;
3862
+ if (Array.isArray(allClients)) {
3863
+ const offset2 = out.length;
3864
+ allClients.forEach(
3865
+ (value, index) => out.push({ value, index: offset2 + index })
3866
+ );
3867
+ return out;
3868
+ }
3869
+ const offset = out.length;
3870
+ Object.entries(allClients).forEach(([registeredName, value], index) => {
3871
+ out.push({ value, registeredName, index: offset + index });
3872
+ });
3873
+ return out;
3874
+ }
3875
+ function looksLikeClientEndpoint(value) {
3876
+ const native = readNativeInspection(value);
3877
+ if (native?.kind === "client-endpoint") return true;
3878
+ return Boolean(
3879
+ value && typeof value === "object" && "leaf" in value && value.leaf
3880
+ );
3881
+ }
3882
+
3883
+ // src/inspection/serializable.ts
3884
+ var MAX_METADATA_DEPTH = 8;
3885
+ function serializeSelectedMetadata(value, depth = 0) {
3886
+ if (depth > MAX_METADATA_DEPTH || value === void 0) return void 0;
3887
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
3888
+ return value;
3889
+ }
3890
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
3891
+ if (typeof value === "bigint") return String(value);
3892
+ if (value instanceof Date) return value.toISOString();
3893
+ if (Array.isArray(value)) {
3894
+ return value.map((item) => serializeSelectedMetadata(item, depth + 1)).filter((item) => item !== void 0);
3895
+ }
3896
+ if (typeof value !== "object") return void 0;
3897
+ const out = {};
3898
+ for (const key of Object.keys(value).sort()) {
3899
+ const child = serializeSelectedMetadata(
3900
+ value[key],
3901
+ depth + 1
3902
+ );
3903
+ if (child !== void 0) out[key] = child;
3904
+ }
3905
+ return out;
3906
+ }
3907
+ var CLIENT_BUILD_OPTION_FIELDS = [
3908
+ "enabled",
3909
+ "staleTime",
3910
+ "gcTime",
3911
+ "retry",
3912
+ "retryDelay",
3913
+ "refetchInterval",
3914
+ "refetchOnMount",
3915
+ "refetchOnWindowFocus",
3916
+ "refetchOnReconnect",
3917
+ "singleton",
3918
+ "splitPageSize",
3919
+ "splitPageSizeParam",
3920
+ "pagination",
3921
+ "feed",
3922
+ "many"
3923
+ ];
3924
+ function serializeClientBuildOptions(value) {
3925
+ if (!value || typeof value !== "object") return {};
3926
+ const source = value;
3927
+ const out = {};
3928
+ for (const field of CLIENT_BUILD_OPTION_FIELDS) {
3929
+ const selected = serializeSelectedMetadata(source[field]);
3930
+ if (selected !== void 0) out[field] = selected;
3931
+ }
3932
+ return out;
3933
+ }
3934
+
3935
+ // src/inspection/inspect-clients.ts
3936
+ function isLeaf(value) {
3937
+ const record = asRecord2(value);
3938
+ return Boolean(
3939
+ record && typeof record.method === "string" && typeof record.path === "string" && record.cfg && typeof record.cfg === "object"
3940
+ );
3941
+ }
3942
+ function endpointFrom(value) {
3943
+ const descriptor = readNativeInspection(value);
3944
+ if (descriptor?.kind === "client-endpoint" && isLeaf(descriptor.leaf)) {
3945
+ return {
3946
+ leaf: descriptor.leaf,
3947
+ id: optionalString(descriptor.id),
3948
+ name: optionalString(descriptor.name),
3949
+ enabled: optionalBoolean(descriptor.enabled) ?? true,
3950
+ built: optionalBoolean(descriptor.built) ?? true,
3951
+ buildOptions: descriptor.buildOptions,
3952
+ augments: Array.isArray(descriptor.augments) ? descriptor.augments.map(asRecord2).filter(
3953
+ (item) => Boolean(item)
3954
+ ) : [],
3955
+ socketConnections: Array.isArray(descriptor.socketConnections) ? descriptor.socketConnections.map(asRecord2).filter(
3956
+ (item) => Boolean(item)
3957
+ ) : [],
3958
+ complete: true
3959
+ };
3960
+ }
3961
+ const record = asRecord2(value);
3962
+ if (!record || !isLeaf(record.leaf)) return void 0;
3963
+ return {
3964
+ leaf: record.leaf,
3965
+ enabled: true,
3966
+ built: true,
3967
+ augments: [],
3968
+ socketConnections: [],
3969
+ complete: false
3970
+ };
3971
+ }
3972
+ function bindingsFromDescriptor(descriptor) {
3973
+ if (descriptor.kind === "client-resource") {
3974
+ const bindings = Array.isArray(descriptor.bindings) ? descriptor.bindings.flatMap((value, index) => {
3975
+ const binding = asRecord2(value);
3976
+ const endpoint = endpointFromInspectionRecord(binding?.endpoint) ?? endpointFrom(binding?.endpoint);
3977
+ if (!binding || !endpoint) return [];
3978
+ return [
3979
+ {
3980
+ id: optionalString(binding.id),
3981
+ publicName: optionalString(binding.publicName) ?? endpoint.name ?? `endpoint-${index}`,
3982
+ enabled: optionalBoolean(binding.enabled),
3983
+ endpoint
3984
+ }
3985
+ ];
3986
+ }) : [];
3987
+ return {
3988
+ id: optionalString(descriptor.id),
3989
+ name: optionalString(descriptor.name),
3990
+ bindings
3991
+ };
3992
+ }
3993
+ if (descriptor.kind === "endpoint-client") {
3994
+ const endpoints = Array.isArray(descriptor.endpoints) ? descriptor.endpoints : [];
3995
+ return {
3996
+ name: "Endpoint client",
3997
+ bindings: endpoints.flatMap((value, index) => {
3998
+ const endpoint = endpointFromInspectionRecord(value);
3999
+ if (!endpoint) return [];
4000
+ return [
4001
+ {
4002
+ id: endpoint.id,
4003
+ publicName: endpoint.name ?? endpoint.id ?? (0, import_rrroutes_contract3.routeKeyOf)(endpoint.leaf.method, endpoint.leaf.path),
4004
+ enabled: endpoint.enabled,
4005
+ endpoint
4006
+ }
4007
+ ];
4008
+ })
4009
+ };
4010
+ }
4011
+ return void 0;
4012
+ }
4013
+ function endpointFromInspectionRecord(value) {
4014
+ const record = asRecord2(value);
4015
+ if (!record || record.kind !== "client-endpoint" || !isLeaf(record.leaf)) {
4016
+ return void 0;
4017
+ }
4018
+ return {
4019
+ leaf: record.leaf,
4020
+ id: optionalString(record.id),
4021
+ name: optionalString(record.name),
4022
+ enabled: optionalBoolean(record.enabled) ?? true,
4023
+ built: optionalBoolean(record.built) ?? true,
4024
+ buildOptions: record.buildOptions,
4025
+ augments: Array.isArray(record.augments) ? record.augments.map(asRecord2).filter((item) => Boolean(item)) : [],
4026
+ socketConnections: Array.isArray(record.socketConnections) ? record.socketConnections.map(asRecord2).filter((item) => Boolean(item)) : [],
4027
+ complete: true
4028
+ };
4029
+ }
4030
+ function resourceFrom(input) {
4031
+ const descriptor = readNativeInspection(input.value);
4032
+ const native = descriptor ? bindingsFromDescriptor(descriptor) : void 0;
4033
+ if (native) return native;
4034
+ if (looksLikeClientEndpoint(input.value)) {
4035
+ const endpoint = endpointFrom(input.value);
4036
+ return {
4037
+ bindings: endpoint ? [
4038
+ {
4039
+ id: endpoint.id,
4040
+ publicName: endpoint.name ?? endpoint.id ?? "endpoint",
4041
+ enabled: endpoint.enabled,
4042
+ endpoint
4043
+ }
4044
+ ] : []
4045
+ };
4046
+ }
4047
+ return {
4048
+ bindings: Object.entries(input.value).flatMap(
4049
+ ([publicName, value]) => {
4050
+ const endpoint = endpointFrom(value);
4051
+ return endpoint ? [{ publicName, endpoint }] : [];
4052
+ }
4053
+ )
4054
+ };
4055
+ }
4056
+ function inspectAugments(descriptors, bindingIdentity) {
4057
+ return descriptors.map((descriptor, index) => {
4058
+ const id = optionalString(descriptor.id);
4059
+ return {
4060
+ identity: id ?? `${bindingIdentity}:augment:${index}`,
4061
+ id,
4062
+ name: optionalString(descriptor.name),
4063
+ kind: optionalString(descriptor.kind),
4064
+ options: serializeSelectedMetadata(descriptor.options),
4065
+ order: index,
4066
+ identityStability: id ? "stable" : "positional"
4067
+ };
4068
+ });
4069
+ }
4070
+ function inspectSocketConnections(descriptors, bindingIdentity) {
4071
+ return descriptors.map((descriptor, index) => {
4072
+ const id = optionalString(descriptor.id);
4073
+ return {
4074
+ identity: id ?? `${bindingIdentity}:socket:${index}`,
4075
+ id,
4076
+ name: optionalString(descriptor.name),
4077
+ events: Array.isArray(descriptor.events) ? descriptor.events.filter((item) => typeof item === "string").sort() : [],
4078
+ roomStrategy: serializeSelectedMetadata(descriptor.roomStrategy),
4079
+ cacheReducers: Array.isArray(descriptor.cacheReducers) ? descriptor.cacheReducers.map((item) => serializeSelectedMetadata(item)).filter(
4080
+ (item) => item !== void 0
4081
+ ) : [],
4082
+ order: index,
4083
+ identityStability: id ? "stable" : "positional"
4084
+ };
4085
+ });
4086
+ }
4087
+ function inspectClients(inputs, diagnostics) {
4088
+ const clients = {};
4089
+ const byRoute = {};
4090
+ for (const input of inputs) {
4091
+ const resource = resourceFrom(input);
4092
+ const identity = resource.id ?? input.registeredName ?? `client-${input.index}`;
4093
+ const resourceStability = resource.id || input.registeredName ? "stable" : "positional";
4094
+ const seenPublicNames = /* @__PURE__ */ new Map();
4095
+ const bindings = resource.bindings.map((binding, index) => {
4096
+ const routeKey = (0, import_rrroutes_contract3.routeKeyOf)(
4097
+ binding.endpoint.leaf.method,
4098
+ binding.endpoint.leaf.path
4099
+ );
4100
+ const seen = seenPublicNames.get(binding.publicName) ?? 0;
4101
+ seenPublicNames.set(binding.publicName, seen + 1);
4102
+ const publicName = seen === 0 ? binding.publicName : `${binding.publicName}-${index}`;
4103
+ const bindingIdentity = binding.id ?? `${identity}:${publicName}`;
4104
+ const buildOptions = serializeClientBuildOptions(
4105
+ binding.endpoint.buildOptions
4106
+ );
4107
+ const pagination = buildOptions.pagination;
4108
+ const inspected = {
4109
+ clientId: identity,
4110
+ clientName: resource.name ?? input.registeredName,
4111
+ bindingId: binding.id,
4112
+ bindingIdentity,
4113
+ publicName,
4114
+ routeKey,
4115
+ enabled: binding.enabled ?? binding.endpoint.enabled,
4116
+ built: binding.endpoint.built,
4117
+ build: {
4118
+ feed: Boolean(binding.endpoint.leaf.cfg.feed),
4119
+ singleton: buildOptions.singleton === true,
4120
+ many: Boolean(buildOptions.many),
4121
+ pagination,
4122
+ options: buildOptions
4123
+ },
4124
+ augments: inspectAugments(binding.endpoint.augments, bindingIdentity),
4125
+ socketConnections: inspectSocketConnections(
4126
+ binding.endpoint.socketConnections,
4127
+ bindingIdentity
4128
+ ),
4129
+ identityStability: binding.id || seen === 0 ? "stable" : "positional"
4130
+ };
4131
+ if (!binding.endpoint.complete) {
4132
+ diagnostics.add({
4133
+ code: "CLIENT_AUGMENT_DETAILS_UNAVAILABLE",
4134
+ severity: "info",
4135
+ entity: { routeKey, client: identity },
4136
+ message: "The endpoint was detected, but augment descriptors were not exposed."
4137
+ });
4138
+ }
4139
+ byRoute[routeKey] ?? (byRoute[routeKey] = []);
4140
+ byRoute[routeKey].push(inspected);
4141
+ return inspected;
4142
+ });
4143
+ clients[identity] = {
4144
+ identity,
4145
+ id: resource.id,
4146
+ name: resource.name ?? input.registeredName,
4147
+ identityStability: resourceStability,
4148
+ bindings
4149
+ };
4150
+ if (resourceStability === "positional") {
4151
+ diagnostics.add({
4152
+ code: "CLIENT_IDENTITY_POSITIONAL",
4153
+ severity: "info",
4154
+ entity: { client: identity },
4155
+ message: "Client identity uses its array position and may change after reordering."
4156
+ });
4157
+ }
4158
+ }
4159
+ for (const bindings of Object.values(byRoute)) {
4160
+ bindings.sort(
4161
+ (left, right) => left.bindingIdentity.localeCompare(right.bindingIdentity)
4162
+ );
4163
+ }
4164
+ return { clients, byRoute };
4165
+ }
4166
+
4167
+ // src/inspection/inspect-contract.ts
4168
+ var import_rrroutes_contract4 = require("@emeryld/rrroutes-contract");
4169
+ async function inspectContractRoutes(leaves, source, includeSource) {
4170
+ const exported = await exportFinalizedLeaves(leaves, {
4171
+ includeSource: includeSource && (source?.include ?? true),
4172
+ sourceModulePath: source?.modulePath,
4173
+ sourceExportName: source?.exportName,
4174
+ tsconfigPath: source?.tsconfigPath
4175
+ });
4176
+ const routes = {};
4177
+ for (const serializedLeaf of exported.leaves) {
4178
+ const routeKey = (0, import_rrroutes_contract4.routeKeyOf)(serializedLeaf.method, serializedLeaf.path);
4179
+ const schemaFlat = exported.schemaFlatByLeaf[routeKey] ?? {};
4180
+ routes[routeKey] = {
4181
+ serializedLeaf,
4182
+ schemaFlat,
4183
+ source: exported.sourceByLeaf?.[routeKey],
4184
+ fingerprint: hashCanonicalValue({ serializedLeaf, schemaFlat })
4185
+ };
4186
+ }
4187
+ return {
4188
+ routes,
4189
+ sourceExtraction: exported._meta.sourceExtraction
4190
+ };
4191
+ }
4192
+
4193
+ // src/inspection/inspect-server.ts
4194
+ function isLeaf2(value) {
4195
+ const record = asRecord2(value);
4196
+ return Boolean(
4197
+ record && typeof record.method === "string" && typeof record.path === "string" && record.cfg && typeof record.cfg === "object"
4198
+ );
4199
+ }
4200
+ function inspectServer(input, diagnostics) {
4201
+ if (!input)
4202
+ return {
4203
+ servers: {},
4204
+ byRoute: {}
4205
+ };
4206
+ const descriptor = readNativeInspection(input);
4207
+ if (!descriptor || descriptor.kind !== "server-runtime") {
4208
+ diagnostics.add({
4209
+ code: "SERVER_REGISTRATION_DETAILS_UNAVAILABLE",
4210
+ severity: "warning",
4211
+ entity: { server: "server" },
4212
+ message: "A server runtime was supplied, but it did not expose the RRRoutes inspection protocol."
4213
+ });
4214
+ return {
4215
+ servers: {
4216
+ server: {
4217
+ identity: "server",
4218
+ identityStability: "stable",
4219
+ controllers: {},
4220
+ middleware: { sanitizerCount: 0, preCtxCount: 0, postCtxCount: 0 },
4221
+ validateOutput: false
4222
+ }
4223
+ },
4224
+ byRoute: {}
4225
+ };
4226
+ }
4227
+ const id = optionalString(descriptor.id);
4228
+ const name = optionalString(descriptor.name);
4229
+ const identity = id ?? name ?? "server";
4230
+ const nativeMiddleware = asRecord2(descriptor.middleware);
4231
+ const middleware = {
4232
+ sanitizerCount: typeof nativeMiddleware?.sanitizerCount === "number" ? nativeMiddleware.sanitizerCount : 0,
4233
+ preCtxCount: typeof nativeMiddleware?.preCtxCount === "number" ? nativeMiddleware.preCtxCount : 0,
4234
+ postCtxCount: typeof nativeMiddleware?.postCtxCount === "number" ? nativeMiddleware.postCtxCount : 0
4235
+ };
4236
+ const validateOutput = optionalBoolean(descriptor.validateOutput) ?? false;
4237
+ const controllers = {};
4238
+ for (const nativeValue of Array.isArray(descriptor.controllers) ? descriptor.controllers : []) {
4239
+ const native = asRecord2(nativeValue);
4240
+ const routeKey = optionalString(native?.routeKey);
4241
+ if (!native || !routeKey || !isLeaf2(native.leaf)) continue;
4242
+ const controllerId = optionalString(native.id);
4243
+ const before = (Array.isArray(native.before) ? native.before : []).flatMap((middlewareValue, index) => {
4244
+ const item = asRecord2(middlewareValue);
4245
+ if (!item) return [];
4246
+ const middlewareId = optionalString(item.id);
4247
+ return [
4248
+ {
4249
+ identity: middlewareId ?? `${routeKey}:before:${index}`,
4250
+ id: middlewareId,
4251
+ name: optionalString(item.name),
4252
+ order: index,
4253
+ identityStability: middlewareId ? "stable" : "positional"
4254
+ }
4255
+ ];
4256
+ });
4257
+ controllers[routeKey] = {
4258
+ routeKey,
4259
+ registered: true,
4260
+ enabled: optionalBoolean(native.enabled) ?? true,
4261
+ controllerId,
4262
+ controllerName: optionalString(native.name),
4263
+ before,
4264
+ middleware: {
4265
+ ...middleware,
4266
+ beforeCount: before.length
4267
+ },
4268
+ validateOutput,
4269
+ multipart: Boolean(native.leaf.cfg.bodyFiles?.length),
4270
+ identityStability: "stable"
4271
+ };
4272
+ for (const item of before) {
4273
+ if (item.identityStability === "positional") {
4274
+ diagnostics.add({
4275
+ code: "SERVER_MIDDLEWARE_IDENTITY_POSITIONAL",
4276
+ severity: "info",
4277
+ entity: { routeKey, server: identity },
4278
+ message: "Middleware identity uses its route-local position and may change after reordering."
4279
+ });
4280
+ }
4281
+ }
4282
+ }
4283
+ return {
4284
+ servers: {
4285
+ [identity]: {
4286
+ identity,
4287
+ id,
4288
+ name,
4289
+ identityStability: "stable",
4290
+ controllers,
4291
+ middleware,
4292
+ validateOutput
4293
+ }
4294
+ },
4295
+ byRoute: controllers
4296
+ };
4297
+ }
4298
+
4299
+ // src/inspection/inspect-sockets.ts
4300
+ function toLiveInspection(value) {
4301
+ const live = asRecord2(value);
4302
+ if (!live) return void 0;
4303
+ const routeSubscriptions = Array.isArray(live.leaves) ? live.leaves.flatMap(
4304
+ (leafValue) => {
4305
+ const leaf = asRecord2(leafValue);
4306
+ const routeKey = optionalString(leaf?.leaf);
4307
+ if (!leaf || !routeKey) return [];
4308
+ return [
4309
+ {
4310
+ routeKey,
4311
+ rooms: Array.isArray(leaf.rooms) ? leaf.rooms.flatMap((roomValue) => {
4312
+ const room = asRecord2(roomValue);
4313
+ return room && typeof room.room === "string" && typeof room.count === "number" ? [{ room: room.room, count: room.count }] : [];
4314
+ }) : [],
4315
+ handlers: Array.isArray(leaf.handlers) ? leaf.handlers.flatMap((handlerValue) => {
4316
+ const handler = asRecord2(handlerValue);
4317
+ return handler && typeof handler.event === "string" && typeof handler.handlers === "number" ? [{ event: handler.event, handlers: handler.handlers }] : [];
4318
+ }) : []
4319
+ }
4320
+ ];
4321
+ }
4322
+ ) : void 0;
4323
+ return {
4324
+ connected: optionalBoolean(live.connected) ?? false,
4325
+ socketId: optionalString(live.socketId),
4326
+ namespace: optionalString(live.nsp),
4327
+ roomsCount: typeof live.roomsCount === "number" ? live.roomsCount : void 0,
4328
+ totalHandlers: typeof live.totalHandlers === "number" ? live.totalHandlers : void 0,
4329
+ rooms: Array.isArray(live.rooms) ? live.rooms.flatMap((roomValue) => {
4330
+ const room = asRecord2(roomValue);
4331
+ return room && typeof room.room === "string" && typeof room.count === "number" ? [{ room: room.room, count: room.count }] : [];
4332
+ }) : void 0,
4333
+ handlers: Array.isArray(live.handlers) ? live.handlers.flatMap((handlerValue) => {
4334
+ const handler = asRecord2(handlerValue);
4335
+ return handler && typeof handler.event === "string" && typeof handler.handlers === "number" ? [{ event: handler.event, handlers: handler.handlers }] : [];
4336
+ }) : void 0,
4337
+ routeSubscriptions
4338
+ };
4339
+ }
4340
+ function inspectSockets(inputs, includeLive, diagnostics) {
4341
+ const sockets = {};
4342
+ for (const input of inputs) {
4343
+ const descriptor = readNativeInspection(input.value);
4344
+ const id = optionalString(descriptor?.id);
4345
+ const name = optionalString(descriptor?.name);
4346
+ const identity = id ?? input.registeredName ?? name ?? `socket-${input.index}`;
4347
+ const stability = id || input.registeredName || name ? "stable" : "positional";
4348
+ const events = asRecord2(descriptor?.events);
4349
+ const contractEvents = {};
4350
+ const handlers = (Array.isArray(descriptor?.handlers) ? descriptor.handlers : []).flatMap((handlerValue, index) => {
4351
+ const handler = asRecord2(handlerValue);
4352
+ const event = optionalString(handler?.event);
4353
+ if (!handler || !event) return [];
4354
+ return [
4355
+ {
4356
+ identity: `${identity}:handler:${event}:${index}`,
4357
+ event,
4358
+ name: optionalString(handler.name),
4359
+ order: typeof handler.order === "number" ? handler.order : index,
4360
+ identityStability: "positional"
4361
+ }
4362
+ ];
4363
+ });
4364
+ const bindings = (Array.isArray(descriptor?.bindings) ? descriptor.bindings : []).flatMap((bindingValue, index) => {
4365
+ const binding = asRecord2(bindingValue);
4366
+ const routeKey = optionalString(binding?.routeKey);
4367
+ if (!binding || !routeKey) return [];
4368
+ return [
4369
+ {
4370
+ identity: `${identity}:binding:${routeKey}`,
4371
+ routeKey,
4372
+ eventNames: Array.isArray(binding.events) ? binding.events.filter((event) => typeof event === "string").sort() : [],
4373
+ identityStability: "stable"
4374
+ }
4375
+ ];
4376
+ });
4377
+ for (const eventName of Object.keys(events ?? {}).sort()) {
4378
+ const event = asRecord2(events?.[eventName]);
4379
+ const message = event?.message;
4380
+ contractEvents[eventName] = {
4381
+ identity: `${identity}:event:${eventName}`,
4382
+ name: eventName,
4383
+ schema: message && typeof message === "object" ? introspectSchema(message) : void 0
4384
+ };
4385
+ }
4386
+ if (!descriptor || descriptor.kind !== "socket-runtime") {
4387
+ diagnostics.add({
4388
+ code: "SOCKET_DETAILS_UNAVAILABLE",
4389
+ severity: "info",
4390
+ entity: { socket: identity },
4391
+ message: "A socket runtime was supplied, but it did not expose the RRRoutes inspection protocol."
4392
+ });
4393
+ }
4394
+ if (stability === "positional") {
4395
+ diagnostics.add({
4396
+ code: "SOCKET_IDENTITY_POSITIONAL",
4397
+ severity: "info",
4398
+ entity: { socket: identity },
4399
+ message: "Socket identity uses its array position and may change after reordering."
4400
+ });
4401
+ }
4402
+ sockets[identity] = {
4403
+ identity,
4404
+ id,
4405
+ name: name ?? input.registeredName,
4406
+ identityStability: stability,
4407
+ contract: { events: contractEvents },
4408
+ configuration: {
4409
+ heartbeat: serializeSelectedMetadata(descriptor?.heartbeat),
4410
+ reconnection: serializeSelectedMetadata(descriptor?.reconnection),
4411
+ system: serializeSelectedMetadata(descriptor?.config),
4412
+ handlers
4413
+ },
4414
+ bindings,
4415
+ live: includeLive ? toLiveInspection(
4416
+ descriptor?.live ?? asRecord2(input.value)?.statsSnapshot
4417
+ ) : void 0
4418
+ };
4419
+ }
4420
+ return sockets;
4421
+ }
4422
+
4423
+ // src/inspection/join-routes.ts
4424
+ function joinRouteInspections(args) {
4425
+ const routes = {};
4426
+ const serverRuntime = Object.values(args.servers)[0];
4427
+ for (const routeKey of Object.keys(args.contract).sort()) {
4428
+ const contract = args.contract[routeKey];
4429
+ const clients = (args.clients[routeKey] ?? []).slice();
4430
+ const registeredServer = args.server[routeKey];
4431
+ const server = registeredServer ?? (args.serverSupplied ? {
4432
+ routeKey,
4433
+ registered: false,
4434
+ enabled: false,
4435
+ before: [],
4436
+ middleware: {
4437
+ sanitizerCount: serverRuntime?.middleware.sanitizerCount ?? 0,
4438
+ preCtxCount: serverRuntime?.middleware.preCtxCount ?? 0,
4439
+ postCtxCount: serverRuntime?.middleware.postCtxCount ?? 0,
4440
+ beforeCount: 0
4441
+ },
4442
+ validateOutput: serverRuntime?.validateOutput ?? false,
4443
+ multipart: Boolean(contract.serializedLeaf.cfg.bodyFiles?.length),
4444
+ identityStability: "stable"
4445
+ } : void 0);
4446
+ const sockets = clients.flatMap(
4447
+ (client) => client.socketConnections.map((connection) => ({
4448
+ identity: connection.identity,
4449
+ clientBindingIdentity: client.bindingIdentity,
4450
+ events: connection.events,
4451
+ roomStrategy: connection.roomStrategy,
4452
+ cacheReducers: connection.cacheReducers,
4453
+ identityStability: connection.identityStability
4454
+ }))
4455
+ );
4456
+ routes[routeKey] = {
4457
+ routeKey,
4458
+ id: contract.serializedLeaf.id,
4459
+ method: contract.serializedLeaf.method.toUpperCase(),
4460
+ path: contract.serializedLeaf.path,
4461
+ contract,
4462
+ clients,
4463
+ server,
4464
+ sockets,
4465
+ status: {
4466
+ client: clients.length === 0 ? "unbound" : clients.some((client) => client.enabled) ? "bound" : "disabled",
4467
+ server: !args.serverSupplied ? "unavailable" : !server?.registered ? "missing" : server.enabled ? "enabled" : "disabled",
4468
+ socket: sockets.length > 0 ? "configured" : "none"
4469
+ }
4470
+ };
4471
+ }
4472
+ for (const routeKey of Object.keys(args.clients)) {
4473
+ if (args.contract[routeKey]) continue;
4474
+ args.diagnostics.push({
4475
+ code: "CLIENT_ROUTE_NOT_IN_CONTRACT_INPUT",
4476
+ severity: "warning",
4477
+ entity: { routeKey },
4478
+ message: "A client binding targets a route that was not present in allRoutes."
4479
+ });
4480
+ }
4481
+ for (const routeKey of Object.keys(args.server)) {
4482
+ if (args.contract[routeKey]) continue;
4483
+ args.diagnostics.push({
4484
+ code: "SERVER_ROUTE_NOT_IN_CONTRACT_INPUT",
4485
+ severity: "warning",
4486
+ entity: { routeKey },
4487
+ message: "A server controller targets a route that was not present in allRoutes."
4488
+ });
4489
+ }
4490
+ return routes;
4491
+ }
4492
+
4493
+ // src/inspection/normalize-routes.ts
4494
+ function hasAll(value) {
4495
+ return Boolean(
4496
+ value && typeof value === "object" && "all" in value && Array.isArray(value.all)
4497
+ );
4498
+ }
4499
+ function normalizeRoutesInput(input) {
4500
+ if (Array.isArray(input)) return input;
4501
+ if (hasAll(input)) return input.all;
4502
+ throw new TypeError(
4503
+ "allRoutes must be a finalized registry, RouteResource, or readonly leaf array."
4504
+ );
4505
+ }
4506
+
4507
+ // src/inspection/normalize-sockets.ts
4508
+ function normalizeSocketsInput(input) {
4509
+ if (!input) return [];
4510
+ if (Array.isArray(input)) {
4511
+ return input.map((value, index) => ({ value, index }));
4512
+ }
4513
+ if (readNativeInspection(input)?.kind === "socket-runtime") {
4514
+ return [{ value: input, index: 0 }];
4515
+ }
4516
+ if ("statsSnapshot" in input || "on" in input && typeof input.on === "function") {
4517
+ return [{ value: input, index: 0 }];
4518
+ }
4519
+ return Object.entries(input).map(([registeredName, value], index) => ({
4520
+ value,
4521
+ registeredName,
4522
+ index
4523
+ }));
4524
+ }
4525
+
4526
+ // src/snapshot/build.ts
4527
+ async function buildRRRoutesSnapshot(options, inspectOptions = {}) {
4528
+ const diagnostics = new InspectionDiagnostics();
4529
+ const leaves = normalizeRoutesInput(options.allRoutes);
4530
+ const seen = /* @__PURE__ */ new Set();
4531
+ for (const leaf of leaves) {
4532
+ const routeKey = (0, import_rrroutes_contract5.routeKeyOf)(leaf.method, leaf.path);
4533
+ if (seen.has(routeKey)) {
4534
+ diagnostics.add({
4535
+ code: "DUPLICATE_ROUTE_KEY",
4536
+ severity: "error",
4537
+ entity: { routeKey },
4538
+ message: "Multiple contract leaves resolve to the same canonical method + path identity."
4539
+ });
4540
+ }
4541
+ seen.add(routeKey);
4542
+ }
4543
+ const includeSource = inspectOptions.includeSource ?? options.source?.include ?? false;
4544
+ const includeLive = inspectOptions.includeLive ?? false;
4545
+ const [contract, clients, server, sockets] = await Promise.all([
4546
+ inspectContractRoutes(leaves, options.source, includeSource),
4547
+ Promise.resolve(
4548
+ inspectClients(
4549
+ normalizeClientsInput(options.endpointClient, options.allClients),
4550
+ diagnostics
4551
+ )
4552
+ ),
4553
+ Promise.resolve(inspectServer(options.server, diagnostics)),
4554
+ Promise.resolve(
4555
+ inspectSockets(
4556
+ normalizeSocketsInput(options.sockets),
4557
+ includeLive,
4558
+ diagnostics
4559
+ )
4560
+ )
4561
+ ]);
4562
+ const diagnosticItems = diagnostics.list({
4563
+ includeInfo: options.diagnostics?.includeInfo
4564
+ });
4565
+ const routes = joinRouteInspections({
4566
+ contract: contract.routes,
4567
+ clients: clients.byRoute,
4568
+ server: server.byRoute,
4569
+ servers: server.servers,
4570
+ serverSupplied: Boolean(options.server),
4571
+ diagnostics: diagnosticItems
4572
+ });
4573
+ diagnosticItems.sort((left, right) => left.code.localeCompare(right.code));
4574
+ const snapshot = {
4575
+ schemaVersion: 1,
4576
+ meta: {
4577
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
4578
+ snapshotHash: "",
4579
+ ...options.meta,
4580
+ packageVersions: {},
4581
+ sourceExtraction: contract.sourceExtraction
4582
+ },
4583
+ capabilities: {
4584
+ contract: true,
4585
+ client: Boolean(options.endpointClient || options.allClients),
4586
+ server: Boolean(options.server),
4587
+ socket: Boolean(options.sockets),
4588
+ live: includeLive,
4589
+ source: includeSource
4590
+ },
4591
+ routes,
4592
+ clients: clients.clients,
4593
+ servers: server.servers,
4594
+ sockets,
4595
+ diagnostics: diagnosticItems
4596
+ };
4597
+ snapshot.meta.snapshotHash = hashRRRoutesSnapshot(snapshot);
4598
+ return snapshot;
4599
+ }
4600
+
4601
+ // src/storage/memory.ts
4602
+ function clone(value) {
4603
+ return structuredClone(value);
4604
+ }
4605
+ function createMemoryInspectionStorage() {
4606
+ const checkpoints = /* @__PURE__ */ new Map();
4607
+ const activeByScope = /* @__PURE__ */ new Map();
4608
+ const changes = /* @__PURE__ */ new Map();
4609
+ return {
4610
+ checkpoints: {
4611
+ async get(id) {
4612
+ const value = checkpoints.get(id);
4613
+ return value ? clone(value) : void 0;
4614
+ },
4615
+ async getActive(scope) {
4616
+ const id = activeByScope.get(scope);
4617
+ const value = id ? checkpoints.get(id) : void 0;
4618
+ return value ? clone(value) : void 0;
4619
+ },
4620
+ async list(scope) {
4621
+ return Array.from(checkpoints.values()).filter((checkpoint) => !scope || checkpoint.scope === scope).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map(
4622
+ (checkpoint) => ({
4623
+ id: checkpoint.id,
4624
+ scope: checkpoint.scope,
4625
+ createdAt: checkpoint.createdAt,
4626
+ updatedAt: checkpoint.updatedAt,
4627
+ snapshotHash: checkpoint.snapshotHash,
4628
+ trigger: clone(checkpoint.trigger),
4629
+ expiresAt: checkpoint.expiresAt,
4630
+ active: activeByScope.get(checkpoint.scope) === checkpoint.id
4631
+ })
4632
+ );
4633
+ },
4634
+ async save(checkpoint) {
4635
+ checkpoints.set(checkpoint.id, clone(checkpoint));
4636
+ },
4637
+ async setActive(scope, checkpointId) {
4638
+ const checkpoint = checkpoints.get(checkpointId);
4639
+ if (!checkpoint || checkpoint.scope !== scope) {
4640
+ throw new Error(
4641
+ `Checkpoint ${checkpointId} does not belong to scope ${scope}.`
4642
+ );
4643
+ }
4644
+ activeByScope.set(scope, checkpointId);
4645
+ },
4646
+ async delete(id) {
4647
+ checkpoints.delete(id);
4648
+ for (const [scope, activeId] of activeByScope) {
4649
+ if (activeId === id) activeByScope.delete(scope);
4650
+ }
4651
+ }
4652
+ },
4653
+ changes: {
4654
+ async get(id) {
4655
+ const value = changes.get(id);
4656
+ return value ? clone(value) : void 0;
4657
+ },
4658
+ async findByTransition(scope, fromHash, toHash) {
4659
+ const value = Array.from(changes.values()).find(
4660
+ (changeSet) => changeSet.scope === scope && changeSet.from.snapshotHash === fromHash && changeSet.to.snapshotHash === toHash
4661
+ );
4662
+ return value ? clone(value) : void 0;
4663
+ },
4664
+ async list(query = {}) {
4665
+ const values = Array.from(changes.values()).filter(
4666
+ (changeSet) => !query.scope || changeSet.scope === query.scope
4667
+ ).sort((left, right) => right.createdAt.localeCompare(left.createdAt));
4668
+ const selected = query.limit ? values.slice(0, query.limit) : values;
4669
+ return selected.map((changeSet) => {
4670
+ const { changes: _changes, ...summary } = changeSet;
4671
+ return clone(summary);
4672
+ });
4673
+ },
4674
+ async save(changeSet) {
4675
+ changes.set(changeSet.id, clone(changeSet));
4676
+ },
4677
+ async delete(id) {
4678
+ changes.delete(id);
4679
+ }
4680
+ }
4681
+ };
4682
+ }
4683
+
4684
+ // src/checkpoint/lock.ts
4685
+ var _tails;
4686
+ var ScopeLock = class {
4687
+ constructor() {
4688
+ __privateAdd(this, _tails, /* @__PURE__ */ new Map());
4689
+ }
4690
+ async run(scope, action) {
4691
+ const previous = __privateGet(this, _tails).get(scope) ?? Promise.resolve();
4692
+ let release;
4693
+ const current = new Promise((resolve) => {
4694
+ release = resolve;
4695
+ });
4696
+ __privateGet(this, _tails).set(scope, current);
4697
+ await previous;
4698
+ try {
4699
+ return await action();
4700
+ } finally {
4701
+ release();
4702
+ if (__privateGet(this, _tails).get(scope) === current) __privateGet(this, _tails).delete(scope);
4703
+ }
4704
+ }
4705
+ };
4706
+ _tails = new WeakMap();
4707
+
4708
+ // src/checkpoint/retention.ts
4709
+ async function applyCheckpointRetention(args) {
4710
+ if (!args.retention) return;
4711
+ const summaries = await args.storage.checkpoints.list(args.scope);
4712
+ const now = args.now ?? Date.now();
4713
+ const deletions = /* @__PURE__ */ new Set();
4714
+ if (args.retention.type === "duration" || args.retention.type === "combined") {
4715
+ for (const checkpoint of summaries) {
4716
+ if (checkpoint.id !== args.activeId && now - Date.parse(checkpoint.updatedAt) > args.retention.maximumAgeMs) {
4717
+ deletions.add(checkpoint.id);
4718
+ }
4719
+ }
4720
+ }
4721
+ if (args.retention.type === "count" || args.retention.type === "combined") {
4722
+ const retained = summaries.filter((item) => !deletions.has(item.id));
4723
+ for (const checkpoint of retained.slice(args.retention.maximum)) {
4724
+ if (checkpoint.id !== args.activeId) deletions.add(checkpoint.id);
4725
+ }
4726
+ }
4727
+ await Promise.all(
4728
+ Array.from(deletions).map((id) => args.storage.checkpoints.delete(id))
4729
+ );
4730
+ }
4731
+ async function applyChangeSetRetention(args) {
4732
+ if (args.retention.type === "forever" || !args.storage.changes.delete) return;
4733
+ const summaries = await args.storage.changes.list({ scope: args.scope });
4734
+ const now = args.now ?? Date.now();
4735
+ const deletions = args.retention.type === "count" ? summaries.slice(args.retention.maximum) : (() => {
4736
+ const maximumAgeMs = args.retention.maximumAgeMs;
4737
+ return summaries.filter(
4738
+ (item) => now - Date.parse(item.createdAt) > maximumAgeMs
4739
+ );
4740
+ })();
4741
+ await Promise.all(
4742
+ deletions.map((item) => args.storage.changes.delete(item.id))
4743
+ );
4744
+ }
4745
+
4746
+ // src/checkpoint/service.ts
4747
+ function createCheckpointService(options) {
4748
+ const storage = options.storage ?? createMemoryInspectionStorage();
4749
+ const lock = new ScopeLock();
4750
+ const defaultScope = options.scope ?? "default";
4751
+ const transaction = (operation) => storage.transaction ? storage.transaction(operation) : operation();
4752
+ const createRecord = (scope, snapshot, trigger) => {
4753
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4754
+ return {
4755
+ id: hashCanonicalValue({
4756
+ scope,
4757
+ snapshotHash: snapshot.meta.snapshotHash,
4758
+ now
4759
+ }),
4760
+ scope,
4761
+ createdAt: now,
4762
+ updatedAt: now,
4763
+ snapshotHash: snapshot.meta.snapshotHash,
4764
+ snapshot,
4765
+ trigger
4766
+ };
4767
+ };
4768
+ const service = {
4769
+ async advance(input = {}) {
4770
+ const scope = input.scope ?? defaultScope;
4771
+ const trigger = input.trigger ?? { type: "manual" };
4772
+ return lock.run(scope, () => transaction(async () => {
4773
+ if (trigger.externalId) {
4774
+ const priorSummary = (await storage.checkpoints.list(scope)).find(
4775
+ (checkpoint2) => checkpoint2.trigger.externalId === trigger.externalId
4776
+ );
4777
+ if (priorSummary) {
4778
+ const checkpoint2 = await storage.checkpoints.get(priorSummary.id);
4779
+ if (checkpoint2) {
4780
+ const changeSummary = (await storage.changes.list({ scope })).find(
4781
+ (changeSet3) => changeSet3.trigger.externalId === trigger.externalId
4782
+ );
4783
+ const changeSet2 = changeSummary ? await storage.changes.get(changeSummary.id) : void 0;
4784
+ return {
4785
+ status: "idempotent_replay",
4786
+ checkpoint: checkpoint2,
4787
+ changeSet: changeSet2
4788
+ };
4789
+ }
4790
+ }
4791
+ }
4792
+ const current = await options.inspect();
4793
+ const active = await storage.checkpoints.getActive(scope);
4794
+ if (!active) {
4795
+ const checkpoint2 = createRecord(scope, current, trigger);
4796
+ await storage.checkpoints.save(checkpoint2);
4797
+ await storage.checkpoints.setActive(scope, checkpoint2.id);
4798
+ return { status: "baseline_created", checkpoint: checkpoint2 };
4799
+ }
4800
+ if (active.snapshotHash === current.meta.snapshotHash) {
4801
+ return { status: "no_changes", checkpoint: active };
4802
+ }
4803
+ const duplicate = await storage.changes.findByTransition(
4804
+ scope,
4805
+ active.snapshotHash,
4806
+ current.meta.snapshotHash
4807
+ );
4808
+ const checkpoint = createRecord(scope, current, trigger);
4809
+ const changeSet = duplicate ?? diffRRRoutesSnapshots(active.snapshot, current, {
4810
+ scope,
4811
+ trigger,
4812
+ fromCheckpointId: active.id,
4813
+ toCheckpointId: checkpoint.id
4814
+ });
4815
+ if (!duplicate) await storage.changes.save(changeSet);
4816
+ await storage.checkpoints.save(checkpoint);
4817
+ await storage.checkpoints.setActive(scope, checkpoint.id);
4818
+ await applyCheckpointRetention({
4819
+ storage,
4820
+ scope,
4821
+ activeId: checkpoint.id,
4822
+ retention: options.retention
4823
+ });
4824
+ await applyChangeSetRetention({
4825
+ storage,
4826
+ scope,
4827
+ retention: options.changeSetRetention ?? { type: "forever" }
4828
+ });
4829
+ return { status: "advanced", checkpoint, changeSet };
4830
+ }));
4831
+ },
4832
+ async create(input = {}) {
4833
+ const scope = input.scope ?? defaultScope;
4834
+ const trigger = input.trigger ?? { type: "manual" };
4835
+ return lock.run(scope, () => transaction(async () => {
4836
+ const checkpoint = createRecord(scope, await options.inspect(), trigger);
4837
+ await storage.checkpoints.save(checkpoint);
4838
+ await storage.checkpoints.setActive(scope, checkpoint.id);
4839
+ await applyCheckpointRetention({
4840
+ storage,
4841
+ scope,
4842
+ activeId: checkpoint.id,
4843
+ retention: options.retention
4844
+ });
4845
+ return checkpoint;
4846
+ }));
4847
+ },
4848
+ get: (id) => storage.checkpoints.get(id),
4849
+ getActive: (scope = defaultScope) => storage.checkpoints.getActive(scope),
4850
+ list: (scope) => storage.checkpoints.list(scope),
4851
+ async delete(id) {
4852
+ const checkpoint = await storage.checkpoints.get(id);
4853
+ if (!checkpoint) return;
4854
+ const active = await storage.checkpoints.getActive(checkpoint.scope);
4855
+ if (active?.id === id) {
4856
+ throw new Error("The active checkpoint cannot be deleted.");
4857
+ }
4858
+ await storage.checkpoints.delete(id);
4859
+ },
4860
+ listChangeSets: (scope) => storage.changes.list({ scope }),
4861
+ getChangeSet: (id) => storage.changes.get(id)
4862
+ };
4863
+ return service;
4864
+ }
4865
+
4866
+ // src/storage/filesystem.ts
4867
+ var import_node_crypto3 = require("crypto");
4868
+ var import_promises4 = __toESM(require("fs/promises"), 1);
4869
+ var import_node_path7 = __toESM(require("path"), 1);
4870
+ function encodeSegment(value) {
4871
+ return Buffer.from(value, "utf8").toString("base64url");
4872
+ }
4873
+ async function exists(file) {
4874
+ try {
4875
+ await import_promises4.default.access(file);
4876
+ return true;
4877
+ } catch {
4878
+ return false;
4879
+ }
4880
+ }
4881
+ async function atomicWriteJson(file, value) {
4882
+ const directory = import_node_path7.default.dirname(file);
4883
+ await import_promises4.default.mkdir(directory, { recursive: true });
4884
+ const temporary = import_node_path7.default.join(
4885
+ directory,
4886
+ `.${import_node_path7.default.basename(file)}.${(0, import_node_crypto3.randomUUID)()}.tmp`
4887
+ );
4888
+ const handle = await import_promises4.default.open(temporary, "wx");
4889
+ try {
4890
+ await handle.writeFile(`${JSON.stringify(value, null, 2)}
4891
+ `, "utf8");
4892
+ await handle.sync();
4893
+ } finally {
4894
+ await handle.close();
4895
+ }
4896
+ await import_promises4.default.rename(temporary, file);
4897
+ try {
4898
+ const directoryHandle = await import_promises4.default.open(directory, "r");
4899
+ try {
4900
+ await directoryHandle.sync();
4901
+ } finally {
4902
+ await directoryHandle.close();
4903
+ }
4904
+ } catch {
4905
+ }
4906
+ }
4907
+ async function readJson(file) {
4908
+ try {
4909
+ return JSON.parse(await import_promises4.default.readFile(file, "utf8"));
4910
+ } catch (error) {
4911
+ if (error.code === "ENOENT") return void 0;
4912
+ throw error;
4913
+ }
4914
+ }
4915
+ async function listJsonFiles(directory) {
4916
+ if (!await exists(directory)) return [];
4917
+ const out = [];
4918
+ const visit = async (current) => {
4919
+ for (const entry of await import_promises4.default.readdir(current, { withFileTypes: true })) {
4920
+ const resolved = import_node_path7.default.join(current, entry.name);
4921
+ if (entry.isDirectory()) await visit(resolved);
4922
+ else if (entry.isFile() && entry.name.endsWith(".json"))
4923
+ out.push(resolved);
4924
+ }
4925
+ };
4926
+ await visit(directory);
4927
+ return out.sort();
4928
+ }
4929
+ function createFileSystemInspectionStorage(options) {
4930
+ const root = import_node_path7.default.resolve(options.directory);
4931
+ const checkpointRoot = import_node_path7.default.join(root, "checkpoints");
4932
+ const changeRoot = import_node_path7.default.join(root, "changes");
4933
+ const checkpointFile = (scope, id) => import_node_path7.default.join(
4934
+ checkpointRoot,
4935
+ encodeSegment(scope),
4936
+ `checkpoint-${encodeSegment(id)}.json`
4937
+ );
4938
+ const activeFile = (scope) => import_node_path7.default.join(checkpointRoot, encodeSegment(scope), "active.json");
4939
+ const changeFile = (changeSet) => {
4940
+ const date = new Date(changeSet.createdAt);
4941
+ const year = String(date.getUTCFullYear()).padStart(4, "0");
4942
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
4943
+ return import_node_path7.default.join(
4944
+ changeRoot,
4945
+ encodeSegment(changeSet.scope),
4946
+ year,
4947
+ month,
4948
+ `${encodeSegment(changeSet.id)}.json`
4949
+ );
4950
+ };
4951
+ const findCheckpoint = async (id) => {
4952
+ const files = (await listJsonFiles(checkpointRoot)).filter(
4953
+ (file) => import_node_path7.default.basename(file) !== "active.json"
4954
+ );
4955
+ for (const file of files) {
4956
+ const checkpoint = await readJson(file);
4957
+ if (checkpoint?.id === id) return checkpoint;
4958
+ }
4959
+ return void 0;
4960
+ };
4961
+ const findChangeSet = async (id) => {
4962
+ for (const file of await listJsonFiles(changeRoot)) {
4963
+ const changeSet = await readJson(file);
4964
+ if (changeSet?.id === id) return changeSet;
4965
+ }
4966
+ return void 0;
4967
+ };
4968
+ return {
4969
+ checkpoints: {
4970
+ get: findCheckpoint,
4971
+ async getActive(scope) {
4972
+ const pointer = await readJson(
4973
+ activeFile(scope)
4974
+ );
4975
+ return pointer ? findCheckpoint(pointer.checkpointId) : void 0;
4976
+ },
4977
+ async list(scope) {
4978
+ const files = (await listJsonFiles(checkpointRoot)).filter(
4979
+ (file) => import_node_path7.default.basename(file) !== "active.json"
4980
+ );
4981
+ const activeIds = /* @__PURE__ */ new Map();
4982
+ const values = [];
4983
+ for (const file of files) {
4984
+ const checkpoint = await readJson(file);
4985
+ if (!checkpoint || scope && checkpoint.scope !== scope) continue;
4986
+ values.push(checkpoint);
4987
+ if (!activeIds.has(checkpoint.scope)) {
4988
+ const pointer = await readJson(
4989
+ activeFile(checkpoint.scope)
4990
+ );
4991
+ if (pointer) activeIds.set(checkpoint.scope, pointer.checkpointId);
4992
+ }
4993
+ }
4994
+ return values.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map((checkpoint) => {
4995
+ const { snapshot: _snapshot, ...summary } = checkpoint;
4996
+ return {
4997
+ ...summary,
4998
+ active: activeIds.get(checkpoint.scope) === checkpoint.id
4999
+ };
5000
+ });
5001
+ },
5002
+ async save(checkpoint) {
5003
+ await atomicWriteJson(
5004
+ checkpointFile(checkpoint.scope, checkpoint.id),
5005
+ checkpoint
5006
+ );
5007
+ },
5008
+ async setActive(scope, checkpointId) {
5009
+ const checkpoint = await findCheckpoint(checkpointId);
5010
+ if (!checkpoint || checkpoint.scope !== scope) {
5011
+ throw new Error(
5012
+ `Checkpoint ${checkpointId} does not belong to scope ${scope}.`
5013
+ );
5014
+ }
5015
+ await atomicWriteJson(activeFile(scope), { checkpointId });
5016
+ },
5017
+ async delete(id) {
5018
+ const checkpoint = await findCheckpoint(id);
5019
+ if (!checkpoint) return;
5020
+ const file = checkpointFile(checkpoint.scope, checkpoint.id);
5021
+ await import_promises4.default.unlink(file).catch((error) => {
5022
+ if (error.code !== "ENOENT") throw error;
5023
+ });
5024
+ }
5025
+ },
5026
+ changes: {
5027
+ get: findChangeSet,
5028
+ async findByTransition(scope, fromHash, toHash) {
5029
+ for (const file of await listJsonFiles(
5030
+ import_node_path7.default.join(changeRoot, encodeSegment(scope))
5031
+ )) {
5032
+ const changeSet = await readJson(file);
5033
+ if (changeSet?.scope === scope && changeSet.from.snapshotHash === fromHash && changeSet.to.snapshotHash === toHash) {
5034
+ return changeSet;
5035
+ }
5036
+ }
5037
+ return void 0;
5038
+ },
5039
+ async list(query = {}) {
5040
+ const directory = query.scope ? import_node_path7.default.join(changeRoot, encodeSegment(query.scope)) : changeRoot;
5041
+ const values = [];
5042
+ for (const file of await listJsonFiles(directory)) {
5043
+ const changeSet = await readJson(file);
5044
+ if (changeSet && (!query.scope || changeSet.scope === query.scope)) {
5045
+ values.push(changeSet);
5046
+ }
5047
+ }
5048
+ const selected = values.sort((left, right) => right.createdAt.localeCompare(left.createdAt)).slice(0, query.limit ?? values.length);
5049
+ return selected.map((changeSet) => {
5050
+ const { changes: _changes, ...summary } = changeSet;
5051
+ return summary;
5052
+ });
5053
+ },
5054
+ async save(changeSet) {
5055
+ const existing = await this.findByTransition(
5056
+ changeSet.scope,
5057
+ changeSet.from.snapshotHash,
5058
+ changeSet.to.snapshotHash
5059
+ );
5060
+ if (existing) return;
5061
+ await atomicWriteJson(changeFile(changeSet), changeSet);
5062
+ },
5063
+ async delete(id) {
5064
+ const changeSet = await findChangeSet(id);
5065
+ if (!changeSet) return;
5066
+ await import_promises4.default.unlink(changeFile(changeSet)).catch((error) => {
5067
+ if (error.code !== "ENOENT") throw error;
5068
+ });
5069
+ }
5070
+ }
5071
+ };
5072
+ }
5073
+
5074
+ // src/storage/sqlite.ts
5075
+ function readRow(row) {
5076
+ if (!row || typeof row !== "object" || typeof row.json !== "string") {
5077
+ return void 0;
5078
+ }
5079
+ return JSON.parse(row.json);
5080
+ }
5081
+ function readRows(rows) {
5082
+ return Array.isArray(rows) ? rows.flatMap((row) => {
5083
+ const value = readRow(row);
5084
+ return value ? [value] : [];
5085
+ }) : [];
5086
+ }
5087
+ function createSQLiteInspectionStorage(options) {
5088
+ const database = options.database;
5089
+ database.exec(`
5090
+ PRAGMA foreign_keys = ON;
5091
+ CREATE TABLE IF NOT EXISTS rrroutes_checkpoints (
5092
+ id TEXT PRIMARY KEY,
5093
+ scope TEXT NOT NULL,
5094
+ updated_at TEXT NOT NULL,
5095
+ json TEXT NOT NULL
5096
+ );
5097
+ CREATE INDEX IF NOT EXISTS rrroutes_checkpoints_scope_updated
5098
+ ON rrroutes_checkpoints(scope, updated_at DESC);
5099
+ CREATE TABLE IF NOT EXISTS rrroutes_active_checkpoints (
5100
+ scope TEXT PRIMARY KEY,
5101
+ checkpoint_id TEXT NOT NULL REFERENCES rrroutes_checkpoints(id)
5102
+ );
5103
+ CREATE TABLE IF NOT EXISTS rrroutes_change_sets (
5104
+ id TEXT PRIMARY KEY,
5105
+ scope TEXT NOT NULL,
5106
+ from_hash TEXT NOT NULL,
5107
+ to_hash TEXT NOT NULL,
5108
+ created_at TEXT NOT NULL,
5109
+ json TEXT NOT NULL,
5110
+ UNIQUE(scope, from_hash, to_hash)
5111
+ );
5112
+ CREATE INDEX IF NOT EXISTS rrroutes_changes_scope_created
5113
+ ON rrroutes_change_sets(scope, created_at DESC);
5114
+ `);
5115
+ const getCheckpoint = database.prepare(
5116
+ "SELECT json FROM rrroutes_checkpoints WHERE id = ?"
5117
+ );
5118
+ const storage = {
5119
+ async transaction(operation) {
5120
+ database.exec("BEGIN IMMEDIATE");
5121
+ try {
5122
+ const result = await operation();
5123
+ database.exec("COMMIT");
5124
+ return result;
5125
+ } catch (error) {
5126
+ database.exec("ROLLBACK");
5127
+ throw error;
5128
+ }
5129
+ },
5130
+ checkpoints: {
5131
+ async get(id) {
5132
+ return readRow(getCheckpoint.get(id));
5133
+ },
5134
+ async getActive(scope) {
5135
+ const statement = database.prepare(`
5136
+ SELECT checkpoint.json AS json
5137
+ FROM rrroutes_active_checkpoints active
5138
+ JOIN rrroutes_checkpoints checkpoint ON checkpoint.id = active.checkpoint_id
5139
+ WHERE active.scope = ?
5140
+ `);
5141
+ return readRow(statement.get(scope));
5142
+ },
5143
+ async list(scope) {
5144
+ const rows = scope ? database.prepare(
5145
+ "SELECT json FROM rrroutes_checkpoints WHERE scope = ? ORDER BY updated_at DESC"
5146
+ ).all(scope) : database.prepare(
5147
+ "SELECT json FROM rrroutes_checkpoints ORDER BY updated_at DESC"
5148
+ ).all();
5149
+ const activeRows = database.prepare(
5150
+ "SELECT scope, checkpoint_id FROM rrroutes_active_checkpoints"
5151
+ ).all();
5152
+ const active = new Map(
5153
+ Array.isArray(activeRows) ? activeRows.map((row) => [row.scope, row.checkpoint_id]) : []
5154
+ );
5155
+ return readRows(rows).map(
5156
+ (checkpoint) => {
5157
+ const { snapshot: _snapshot, ...summary } = checkpoint;
5158
+ return {
5159
+ ...summary,
5160
+ active: active.get(checkpoint.scope) === checkpoint.id
5161
+ };
5162
+ }
5163
+ );
5164
+ },
5165
+ async save(checkpoint) {
5166
+ database.prepare(
5167
+ `
5168
+ INSERT INTO rrroutes_checkpoints(id, scope, updated_at, json)
5169
+ VALUES (?, ?, ?, ?)
5170
+ ON CONFLICT(id) DO UPDATE SET
5171
+ scope = excluded.scope,
5172
+ updated_at = excluded.updated_at,
5173
+ json = excluded.json
5174
+ `
5175
+ ).run(
5176
+ checkpoint.id,
5177
+ checkpoint.scope,
5178
+ checkpoint.updatedAt,
5179
+ JSON.stringify(checkpoint)
5180
+ );
5181
+ },
5182
+ async setActive(scope, checkpointId) {
5183
+ const checkpoint = readRow(
5184
+ getCheckpoint.get(checkpointId)
5185
+ );
5186
+ if (!checkpoint || checkpoint.scope !== scope) {
5187
+ throw new Error(
5188
+ `Checkpoint ${checkpointId} does not belong to scope ${scope}.`
5189
+ );
5190
+ }
5191
+ database.prepare(
5192
+ `
5193
+ INSERT INTO rrroutes_active_checkpoints(scope, checkpoint_id)
5194
+ VALUES (?, ?)
5195
+ ON CONFLICT(scope) DO UPDATE SET checkpoint_id = excluded.checkpoint_id
5196
+ `
5197
+ ).run(scope, checkpointId);
5198
+ },
5199
+ async delete(id) {
5200
+ database.prepare("DELETE FROM rrroutes_checkpoints WHERE id = ?").run(id);
5201
+ }
5202
+ },
5203
+ changes: {
5204
+ async get(id) {
5205
+ return readRow(
5206
+ database.prepare("SELECT json FROM rrroutes_change_sets WHERE id = ?").get(id)
5207
+ );
5208
+ },
5209
+ async findByTransition(scope, fromHash, toHash) {
5210
+ return readRow(
5211
+ database.prepare(
5212
+ `
5213
+ SELECT json FROM rrroutes_change_sets
5214
+ WHERE scope = ? AND from_hash = ? AND to_hash = ?
5215
+ `
5216
+ ).get(scope, fromHash, toHash)
5217
+ );
5218
+ },
5219
+ async list(query = {}) {
5220
+ const rows = query.scope ? database.prepare(
5221
+ `
5222
+ SELECT json FROM rrroutes_change_sets
5223
+ WHERE scope = ? ORDER BY created_at DESC LIMIT ?
5224
+ `
5225
+ ).all(query.scope, query.limit ?? -1) : database.prepare(
5226
+ `
5227
+ SELECT json FROM rrroutes_change_sets
5228
+ ORDER BY created_at DESC LIMIT ?
5229
+ `
5230
+ ).all(query.limit ?? -1);
5231
+ return readRows(rows).map(
5232
+ (changeSet) => {
5233
+ const { changes: _changes, ...summary } = changeSet;
5234
+ return summary;
5235
+ }
5236
+ );
5237
+ },
5238
+ async save(changeSet) {
5239
+ database.prepare(
5240
+ `
5241
+ INSERT OR IGNORE INTO rrroutes_change_sets(
5242
+ id, scope, from_hash, to_hash, created_at, json
5243
+ ) VALUES (?, ?, ?, ?, ?, ?)
5244
+ `
5245
+ ).run(
5246
+ changeSet.id,
5247
+ changeSet.scope,
5248
+ changeSet.from.snapshotHash,
5249
+ changeSet.to.snapshotHash,
5250
+ changeSet.createdAt,
5251
+ JSON.stringify(changeSet)
5252
+ );
5253
+ },
5254
+ async delete(id) {
5255
+ database.prepare("DELETE FROM rrroutes_change_sets WHERE id = ?").run(id);
5256
+ }
5257
+ }
5258
+ };
5259
+ return storage;
5260
+ }
5261
+
5262
+ // src/http/handler.ts
5263
+ function json(value, status = 200) {
5264
+ return Response.json(value, {
5265
+ status,
5266
+ headers: {
5267
+ "cache-control": "no-store, max-age=0",
5268
+ "content-type": "application/json; charset=utf-8",
5269
+ "x-content-type-options": "nosniff"
5270
+ }
5271
+ });
5272
+ }
5273
+ async function overview(snapshot, runtime) {
5274
+ const routes = Object.values(snapshot.routes);
5275
+ const clientBindings = Object.values(snapshot.clients).flatMap(
5276
+ (client) => client.bindings
5277
+ );
5278
+ const [activeCheckpoint, latestChangeSet] = await Promise.all([
5279
+ runtime.checkpoints.getActive(),
5280
+ runtime.checkpoints.listChangeSets().then((items) => items[0])
5281
+ ]);
5282
+ return {
5283
+ totalRoutes: routes.length,
5284
+ contractOnlyRoutes: routes.filter(
5285
+ (route) => route.clients.length === 0 && route.status.server !== "enabled"
5286
+ ).length,
5287
+ clientBoundRoutes: routes.filter((route) => route.clients.length > 0).length,
5288
+ serverRegisteredRoutes: routes.filter((route) => route.server?.registered).length,
5289
+ disabledServerRoutes: routes.filter(
5290
+ (route) => route.server?.registered && !route.server.enabled
5291
+ ).length,
5292
+ routesMissingServerController: routes.filter(
5293
+ (route) => route.status.server === "missing"
5294
+ ).length,
5295
+ clientBindings: clientBindings.length,
5296
+ augments: clientBindings.reduce(
5297
+ (total, binding) => total + binding.augments.length,
5298
+ 0
5299
+ ),
5300
+ routeSocketConnections: clientBindings.reduce(
5301
+ (total, binding) => total + binding.socketConnections.length,
5302
+ 0
5303
+ ),
5304
+ socketContractEvents: Object.values(snapshot.sockets).reduce(
5305
+ (total, socket) => total + Object.keys(socket.contract.events).length,
5306
+ 0
5307
+ ),
5308
+ serverBeforeMiddleware: routes.reduce(
5309
+ (total, route) => total + (route.server?.before.length ?? 0),
5310
+ 0
5311
+ ),
5312
+ activeCheckpoint: activeCheckpoint ? {
5313
+ id: activeCheckpoint.id,
5314
+ scope: activeCheckpoint.scope,
5315
+ snapshotHash: activeCheckpoint.snapshotHash,
5316
+ updatedAt: activeCheckpoint.updatedAt
5317
+ } : void 0,
5318
+ latestChangeSet,
5319
+ diagnostics: snapshot.diagnostics.length
5320
+ };
5321
+ }
5322
+ async function readMutationBody(request, maximum) {
5323
+ const contentLength = Number(request.headers.get("content-length") ?? "0");
5324
+ if (contentLength > maximum) throw new Error("REQUEST_BODY_TOO_LARGE");
5325
+ const text = await request.text();
5326
+ if (new TextEncoder().encode(text).byteLength > maximum) {
5327
+ throw new Error("REQUEST_BODY_TOO_LARGE");
5328
+ }
5329
+ if (!text) return {};
5330
+ const value = JSON.parse(text);
5331
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5332
+ throw new Error("REQUEST_BODY_INVALID");
5333
+ }
5334
+ return value;
5335
+ }
5336
+ function createInspectorHttpHandler(runtime, options = {}) {
5337
+ const mutationOptions = options.mutations;
5338
+ const authorizeRead = async (request) => !options.authorize || await options.authorize({ request, action: "read" });
5339
+ const authorizeMutation = async (request, action) => mutationOptions !== false && mutationOptions?.enabled === true && (!mutationOptions.authorize || await mutationOptions.authorize({ request, action }));
5340
+ return async (request) => {
5341
+ try {
5342
+ if (!await authorizeRead(request)) {
5343
+ return json(
5344
+ { error: { code: "FORBIDDEN", message: "Access denied." } },
5345
+ 403
5346
+ );
5347
+ }
5348
+ const url = new URL(request.url);
5349
+ const pathname = url.pathname.replace(/\/$/, "") || "/";
5350
+ const method = request.method.toUpperCase();
5351
+ const snapshot = async () => runtime.inspect({ includeLive: options.includeLive ?? false });
5352
+ if (method === "GET" && pathname === "/api/meta") {
5353
+ const value = await snapshot();
5354
+ return json({ meta: value.meta, capabilities: value.capabilities });
5355
+ }
5356
+ if (method === "GET" && pathname === "/api/snapshot")
5357
+ return json(await snapshot());
5358
+ if (method === "GET" && pathname === "/api/overview")
5359
+ return json(await overview(await snapshot(), runtime));
5360
+ if (method === "GET" && pathname === "/api/routes") {
5361
+ return json(Object.values((await snapshot()).routes));
5362
+ }
5363
+ if (method === "GET" && pathname.startsWith("/api/routes/")) {
5364
+ const routeKey = decodeURIComponent(
5365
+ pathname.slice("/api/routes/".length)
5366
+ );
5367
+ const route = (await snapshot()).routes[routeKey];
5368
+ return route ? json(route) : json(
5369
+ { error: { code: "NOT_FOUND", message: "Route not found." } },
5370
+ 404
5371
+ );
5372
+ }
5373
+ if (method === "GET" && pathname === "/api/clients") {
5374
+ return json((await snapshot()).clients);
5375
+ }
5376
+ if (method === "GET" && pathname.startsWith("/api/clients/")) {
5377
+ const id = decodeURIComponent(pathname.slice("/api/clients/".length));
5378
+ const client = (await snapshot()).clients[id];
5379
+ return client ? json(client) : json(
5380
+ { error: { code: "NOT_FOUND", message: "Client not found." } },
5381
+ 404
5382
+ );
5383
+ }
5384
+ if (method === "GET" && pathname === "/api/servers")
5385
+ return json((await snapshot()).servers);
5386
+ if (method === "GET" && pathname === "/api/sockets")
5387
+ return json((await snapshot()).sockets);
5388
+ if (method === "GET" && pathname === "/api/checkpoints")
5389
+ return json(await runtime.checkpoints.list());
5390
+ if (method === "GET" && pathname === "/api/changes")
5391
+ return json(await runtime.checkpoints.listChangeSets());
5392
+ if (method === "GET" && pathname.startsWith("/api/changes/")) {
5393
+ const id = decodeURIComponent(pathname.slice("/api/changes/".length));
5394
+ const changeSet = await runtime.checkpoints.getChangeSet(id);
5395
+ return changeSet ? json(changeSet) : json(
5396
+ {
5397
+ error: { code: "NOT_FOUND", message: "Change set not found." }
5398
+ },
5399
+ 404
5400
+ );
5401
+ }
5402
+ if (method === "POST" && pathname === "/api/checkpoints") {
5403
+ if (!await authorizeMutation(request, "checkpoint.create")) {
5404
+ return json(
5405
+ {
5406
+ error: {
5407
+ code: "MUTATIONS_DISABLED",
5408
+ message: "Inspector mutations are disabled."
5409
+ }
5410
+ },
5411
+ 403
5412
+ );
5413
+ }
5414
+ const body = await readMutationBody(
5415
+ request,
5416
+ options.maxMutationBodyBytes ?? 16384
5417
+ );
5418
+ return json(
5419
+ await runtime.checkpoints.create({
5420
+ scope: typeof body.scope === "string" ? body.scope : void 0,
5421
+ trigger: body.trigger
5422
+ }),
5423
+ 201
5424
+ );
5425
+ }
5426
+ if (method === "POST" && pathname === "/api/checkpoints/advance") {
5427
+ if (!await authorizeMutation(request, "checkpoint.advance")) {
5428
+ return json(
5429
+ {
5430
+ error: {
5431
+ code: "MUTATIONS_DISABLED",
5432
+ message: "Inspector mutations are disabled."
5433
+ }
5434
+ },
5435
+ 403
5436
+ );
5437
+ }
5438
+ const body = await readMutationBody(
5439
+ request,
5440
+ options.maxMutationBodyBytes ?? 16384
5441
+ );
5442
+ const idempotencyKey = request.headers.get("idempotency-key") ?? void 0;
5443
+ const trigger = body.trigger && typeof body.trigger === "object" ? {
5444
+ ...body.trigger,
5445
+ externalId: body.trigger.externalId ?? idempotencyKey
5446
+ } : { type: "api", externalId: idempotencyKey };
5447
+ return json(
5448
+ await runtime.checkpoints.advance({
5449
+ scope: typeof body.scope === "string" ? body.scope : void 0,
5450
+ trigger
5451
+ })
5452
+ );
5453
+ }
5454
+ if (method === "DELETE" && pathname.startsWith("/api/checkpoints/")) {
5455
+ if (!await authorizeMutation(request, "checkpoint.delete")) {
5456
+ return json(
5457
+ {
5458
+ error: {
5459
+ code: "MUTATIONS_DISABLED",
5460
+ message: "Inspector mutations are disabled."
5461
+ }
5462
+ },
5463
+ 403
5464
+ );
5465
+ }
5466
+ const id = decodeURIComponent(
5467
+ pathname.slice("/api/checkpoints/".length)
5468
+ );
5469
+ await runtime.checkpoints.delete(id);
5470
+ return new Response(null, {
5471
+ status: 204,
5472
+ headers: { "cache-control": "no-store" }
5473
+ });
5474
+ }
5475
+ return json(
5476
+ {
5477
+ error: {
5478
+ code: "NOT_FOUND",
5479
+ message: "Inspector endpoint not found."
5480
+ }
5481
+ },
5482
+ 404
5483
+ );
5484
+ } catch (error) {
5485
+ const message = error instanceof Error ? error.message : String(error);
5486
+ if (message === "REQUEST_BODY_TOO_LARGE") {
5487
+ return json(
5488
+ {
5489
+ error: {
5490
+ code: message,
5491
+ message: "Mutation body exceeds the configured limit."
5492
+ }
5493
+ },
5494
+ 413
5495
+ );
5496
+ }
5497
+ if (message === "REQUEST_BODY_INVALID" || error instanceof SyntaxError) {
5498
+ return json(
5499
+ {
5500
+ error: {
5501
+ code: "REQUEST_BODY_INVALID",
5502
+ message: "Mutation body must be a JSON object."
5503
+ }
5504
+ },
5505
+ 400
5506
+ );
5507
+ }
5508
+ return json({ error: { code: "INSPECTOR_ERROR", message } }, 500);
5509
+ }
5510
+ };
5511
+ }
5512
+
5513
+ // src/adapters/express.ts
5514
+ var import_node_module = require("module");
5515
+ function loadExpress() {
5516
+ const require2 = (0, import_node_module.createRequire)(`${process.cwd()}/package.json`);
5517
+ const loaded = require2("express");
5518
+ return "default" in loaded ? loaded.default : loaded;
5519
+ }
5520
+ function createExpressRouter(options) {
5521
+ const express = loadExpress();
5522
+ const router = express.Router();
5523
+ const limit = options.maxMutationBodyBytes ?? 16384;
5524
+ router.use(express.json({ limit }));
5525
+ const handler = createInspectorHttpHandler(options.runtime, options);
5526
+ const bridge = async (req, res, next) => {
5527
+ try {
5528
+ const headers = new Headers();
5529
+ for (const [name, raw] of Object.entries(req.headers)) {
5530
+ if (Array.isArray(raw))
5531
+ raw.forEach((value) => headers.append(name, value));
5532
+ else if (raw !== void 0) headers.set(name, raw);
5533
+ }
5534
+ const method = req.method.toUpperCase();
5535
+ const body = method === "GET" || method === "HEAD" || req.body === void 0 ? void 0 : JSON.stringify(req.body);
5536
+ const request = new Request(`http://rrroutes.local${req.url}`, {
5537
+ method,
5538
+ headers,
5539
+ body
5540
+ });
5541
+ const response = await handler(request);
5542
+ response.headers.forEach((value, name) => res.setHeader(name, value));
5543
+ res.status(response.status);
5544
+ const bytes = new Uint8Array(await response.arrayBuffer());
5545
+ res.send(Buffer.from(bytes));
5546
+ } catch (error) {
5547
+ next(error);
5548
+ }
5549
+ };
5550
+ router.use(bridge);
5551
+ return router;
5552
+ }
5553
+
5554
+ // src/files/write.ts
5555
+ var import_promises5 = __toESM(require("fs/promises"), 1);
5556
+ var import_node_path8 = __toESM(require("path"), 1);
5557
+ async function writeJson(file, value) {
5558
+ const resolved = import_node_path8.default.resolve(file);
5559
+ await import_promises5.default.mkdir(import_node_path8.default.dirname(resolved), { recursive: true });
5560
+ await import_promises5.default.writeFile(resolved, `${JSON.stringify(value, null, 2)}
5561
+ `, "utf8");
5562
+ }
5563
+ async function writeRRRoutesSnapshot(snapshot, options) {
5564
+ await writeJson(options.outFile, snapshot);
5565
+ }
5566
+ async function writeRRRoutesChangeSet(changeSet, options) {
5567
+ await writeJson(options.outFile, changeSet);
5568
+ }
5569
+
5570
+ // src/runtime.ts
5571
+ function createRRRoutesExportRuntime(options) {
5572
+ const inspect = (inspectOptions) => buildRRRoutesSnapshot(options, inspectOptions);
5573
+ const checkpoints = createCheckpointService({
5574
+ inspect: () => inspect({ includeLive: false }),
5575
+ storage: options.storage,
5576
+ scope: options.checkpoints?.scope,
5577
+ retention: options.checkpoints?.retention,
5578
+ changeSetRetention: options.checkpoints?.changeSetRetention
5579
+ });
5580
+ const runtime = {
5581
+ inspect,
5582
+ async diff(before, after, diffOptions) {
5583
+ return diffRRRoutesSnapshots(
5584
+ before,
5585
+ after ?? await inspect(),
5586
+ diffOptions
5587
+ );
5588
+ },
5589
+ checkpoints,
5590
+ createHttpHandler(httpOptions) {
5591
+ return createInspectorHttpHandler(runtime, httpOptions);
5592
+ },
5593
+ createExpressRouter(expressOptions = {}) {
5594
+ return createExpressRouter({ runtime, ...expressOptions });
5595
+ },
5596
+ async writeSnapshot(writeOptions) {
5597
+ const snapshot = await inspect();
5598
+ await writeRRRoutesSnapshot(snapshot, writeOptions);
5599
+ return snapshot;
5600
+ },
5601
+ writeChangeSet
5602
+ };
5603
+ return runtime;
5604
+ }
5605
+ async function writeChangeSet(changeSet, writeOptions) {
5606
+ await writeRRRoutesChangeSet(changeSet, writeOptions);
5607
+ }
5608
+
5609
+ // src/cli/runtime.ts
5610
+ var import_node_path9 = __toESM(require("path"), 1);
5611
+ var import_node_url3 = require("url");
5612
+ function parseCliFlags(argv) {
5613
+ const flags = /* @__PURE__ */ new Map();
5614
+ for (let index = 0; index < argv.length; index += 1) {
5615
+ const flag = argv[index];
5616
+ if (!flag?.startsWith("--")) continue;
5617
+ const value = argv[index + 1];
5618
+ if (!value || value.startsWith("--")) {
5619
+ flags.set(flag, "true");
5620
+ continue;
5621
+ }
5622
+ flags.set(flag, value);
5623
+ index += 1;
5624
+ }
5625
+ return flags;
5626
+ }
5627
+ async function loadExportRuntime(args) {
5628
+ const resolved = import_node_path9.default.resolve(args.modulePath);
5629
+ const module2 = await import((0, import_node_url3.pathToFileURL)(resolved).href);
5630
+ const exportName = args.exportName ?? "rrroutesExport";
5631
+ const value = module2[exportName] ?? module2.default?.[exportName];
5632
+ if (!value || typeof value.inspect !== "function" || !value.checkpoints) {
5633
+ throw new Error(
5634
+ `Export "${exportName}" in ${resolved} is not an RRRoutes export runtime.`
5635
+ );
5636
+ }
5637
+ return value;
5638
+ }
5639
+ function requiredFlag(flags, name) {
5640
+ const value = flags.get(name);
5641
+ if (!value || value === "true")
5642
+ throw new Error(`Missing required ${name} argument.`);
5643
+ return value;
5644
+ }
5645
+
5646
+ // src/cli/snapshot.ts
5647
+ var import_node_path10 = __toESM(require("path"), 1);
5648
+ async function runSnapshotCli(argv) {
5649
+ const flags = parseCliFlags(argv);
5650
+ const runtime = await loadExportRuntime({
5651
+ modulePath: requiredFlag(flags, "--module"),
5652
+ exportName: flags.get("--export")
5653
+ });
5654
+ const outFile = flags.get("--out") ?? "rrroutes.snapshot.json";
5655
+ const snapshot = await runtime.writeSnapshot({ outFile });
5656
+ return { snapshot, outFile: import_node_path10.default.resolve(outFile) };
5657
+ }
5658
+
5659
+ // src/cli/changes.ts
5660
+ var import_promises6 = __toESM(require("fs/promises"), 1);
5661
+ var import_node_path11 = __toESM(require("path"), 1);
5662
+ async function runChangesCli(argv) {
5663
+ const flags = parseCliFlags(argv);
5664
+ const runtime = await loadExportRuntime({
5665
+ modulePath: requiredFlag(flags, "--module"),
5666
+ exportName: flags.get("--export")
5667
+ });
5668
+ const beforeFile = requiredFlag(flags, "--before");
5669
+ const before = JSON.parse(
5670
+ await import_promises6.default.readFile(import_node_path11.default.resolve(beforeFile), "utf8")
5671
+ );
5672
+ const changeSet = await runtime.diff(before, void 0, {
5673
+ scope: flags.get("--scope"),
5674
+ trigger: { type: "cli" }
5675
+ });
5676
+ const outFile = flags.get("--out") ?? "rrroutes.changes.json";
5677
+ await runtime.writeChangeSet(changeSet, { outFile });
5678
+ return { changeSet, outFile: import_node_path11.default.resolve(outFile) };
5679
+ }
5680
+
5681
+ // src/cli/checkpoint.ts
5682
+ async function runCheckpointCli(argv) {
5683
+ const [action = "advance", ...rest] = argv;
5684
+ const flags = parseCliFlags(rest);
5685
+ const runtime = await loadExportRuntime({
5686
+ modulePath: requiredFlag(flags, "--module"),
5687
+ exportName: flags.get("--export")
5688
+ });
5689
+ const scope = flags.get("--scope");
5690
+ if (action === "advance") {
5691
+ return runtime.checkpoints.advance({ scope, trigger: { type: "cli" } });
5692
+ }
5693
+ if (action === "create") {
5694
+ return runtime.checkpoints.create({ scope, trigger: { type: "cli" } });
5695
+ }
5696
+ throw new Error(`Unsupported checkpoint action: ${action}`);
5697
+ }
5698
+
5699
+ // src/cli/serve.ts
5700
+ var import_node_module2 = require("module");
5701
+ async function runInspectorServeCli(argv) {
5702
+ const loaded = (0, import_node_module2.createRequire)(`${process.cwd()}/package.json`)("express");
5703
+ const express = "default" in loaded ? loaded.default : loaded;
5704
+ const flags = parseCliFlags(argv);
5705
+ const runtime = await loadExportRuntime({
5706
+ modulePath: requiredFlag(flags, "--module"),
5707
+ exportName: flags.get("--export")
5708
+ });
5709
+ const port = Number(flags.get("--port") ?? "4310");
5710
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
5711
+ throw new Error("--port must be an integer between 1 and 65535.");
5712
+ }
5713
+ const app = express();
5714
+ app.use("/", runtime.createExpressRouter());
5715
+ const server = await new Promise((resolve) => {
5716
+ const listening = app.listen(port, () => resolve(listening));
5717
+ });
5718
+ return { app, server, port };
5719
+ }
2774
5720
  // Annotate the CommonJS export names for ESM import in node:
2775
5721
  0 && (module.exports = {
2776
5722
  DEFAULT_VIEWER_TEMPLATE,
5723
+ RRROUTES_BAKED_PAYLOAD_MARKER,
2777
5724
  __private,
5725
+ applyChangeSetRetention,
5726
+ applyCheckpointRetention,
2778
5727
  buildFinalizedLeavesViewerBundle,
5728
+ buildRRRoutesSnapshot,
5729
+ canonicalSnapshotStructure,
5730
+ canonicalizeJson,
2779
5731
  clearSchemaIntrospectionHandlers,
5732
+ createBakedInspectorPayload,
5733
+ createCheckpointService,
5734
+ createExpressRouter,
5735
+ createFileSystemInspectionStorage,
5736
+ createInspectorHttpHandler,
5737
+ createMemoryInspectionStorage,
5738
+ createRRRoutesExportRuntime,
5739
+ createSQLiteInspectionStorage,
2780
5740
  createSchemaIntrospector,
5741
+ createSnapshotChangeSet,
5742
+ diffRRRoutesSnapshots,
5743
+ escapeBakedPayload,
2781
5744
  exportFinalizedLeaves,
2782
5745
  exportFinalizedLeavesChangelog,
2783
5746
  extractLeafSourceByAst,
2784
5747
  flattenLeafSchemas,
2785
5748
  flattenSerializableSchema,
5749
+ hashCanonicalValue,
5750
+ hashRRRoutesSnapshot,
5751
+ injectBakedInspectorPayload,
2786
5752
  introspectSchema,
5753
+ loadExportRuntime,
2787
5754
  loadFinalizedLeavesInput,
5755
+ parseCliFlags,
2788
5756
  parseFinalizedLeavesChangelogCliArgs,
2789
5757
  parseFinalizedLeavesCliArgs,
2790
5758
  registerSchemaIntrospectionHandler,
5759
+ requiredFlag,
5760
+ runChangesCli,
5761
+ runCheckpointCli,
2791
5762
  runExportFinalizedLeavesChangelogCli,
2792
5763
  runExportFinalizedLeavesCli,
5764
+ runInspectorServeCli,
5765
+ runSnapshotCli,
2793
5766
  serializableSchemaKinds,
2794
5767
  serializeLeafContract,
2795
5768
  serializeLeavesContract,
5769
+ stableStringify,
2796
5770
  writeFinalizedLeavesChangelogExport,
2797
5771
  writeFinalizedLeavesExport,
2798
- writeFinalizedLeavesViewerBundle
5772
+ writeFinalizedLeavesViewerBundle,
5773
+ writeRRRoutesChangeSet,
5774
+ writeRRRoutesSnapshot
2799
5775
  });
2800
5776
  //# sourceMappingURL=index.cjs.map