@emeryld/rrroutes-export 1.0.17 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@emeryld/rrroutes-export",
3
3
  "description": "Finalized leaves export helpers and CLI for RRRoutes",
4
- "version": "1.0.17",
4
+ "version": "1.0.19",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "dist/index.cjs",
@@ -24,7 +24,8 @@
24
24
 
25
25
  body {
26
26
  margin: 0;
27
- font-family: 'Iosevka Web', 'SFMono-Regular', Menlo, Consolas, monospace;
27
+ font-family:
28
+ 'Iosevka Web', 'SFMono-Regular', Menlo, Consolas, monospace;
28
29
  color: var(--text);
29
30
  background: var(--bg);
30
31
  }
@@ -61,7 +62,10 @@
61
62
  .primary-row {
62
63
  display: grid;
63
64
  gap: 10px;
64
- grid-template-columns: minmax(220px, 0.9fr) minmax(300px, 1.4fr) minmax(220px, 1fr);
65
+ grid-template-columns: minmax(220px, 0.9fr) minmax(300px, 1.4fr) minmax(
66
+ 220px,
67
+ 1fr
68
+ );
65
69
  align-items: end;
66
70
  }
67
71
 
@@ -374,6 +378,31 @@
374
378
  gap: 4px;
375
379
  }
376
380
 
381
+ .changelog-diff-line {
382
+ display: flex;
383
+ flex-wrap: wrap;
384
+ gap: 6px;
385
+ align-items: baseline;
386
+ }
387
+
388
+ .changelog-op.added {
389
+ font-weight: 700;
390
+ color: var(--ok);
391
+ }
392
+
393
+ .changelog-op.changed {
394
+ font-weight: 700;
395
+ color: var(--accent);
396
+ }
397
+
398
+ .changelog-op.removed {
399
+ color: var(--danger);
400
+ }
401
+
402
+ .changelog-schema-type {
403
+ color: var(--schema-accent);
404
+ }
405
+
377
406
  .schema-block {
378
407
  border-top: 1px solid var(--border);
379
408
  padding: 8px 0 2px;
@@ -497,15 +526,29 @@
497
526
  </label>
498
527
  <label class="control-block">
499
528
  <span class="control-label">Search text</span>
500
- <input id="searchInput" type="text" placeholder="Type to search..." />
529
+ <input
530
+ id="searchInput"
531
+ type="text"
532
+ placeholder="Type to search..."
533
+ />
501
534
  </label>
502
535
  <div class="control-block">
503
536
  <span class="control-label">Quick filters</span>
504
537
  <div class="quick-toggles">
505
- <button id="caseSensitive" class="pill-toggle" type="button" aria-pressed="false">
538
+ <button
539
+ id="caseSensitive"
540
+ class="pill-toggle"
541
+ type="button"
542
+ aria-pressed="false"
543
+ >
506
544
  Case sensitive
507
545
  </button>
508
- <button id="regexSearch" class="pill-toggle" type="button" aria-pressed="false">
546
+ <button
547
+ id="regexSearch"
548
+ class="pill-toggle"
549
+ type="button"
550
+ aria-pressed="false"
551
+ >
509
552
  Regex
510
553
  </button>
511
554
  </div>
@@ -514,11 +557,21 @@
514
557
 
515
558
  <div class="scope-row">
516
559
  <div class="scope-actions">
517
- <button id="selectAllFields" class="scope-action" type="button">All</button>
518
- <button id="clearAllFields" class="scope-action" type="button">None</button>
519
- <button id="coreFields" class="scope-action" type="button">Core</button>
520
- <button id="schemasOnlyFields" class="scope-action" type="button">Schema</button>
521
- <button id="sourceOnlyFields" class="scope-action" type="button">Source</button>
560
+ <button id="selectAllFields" class="scope-action" type="button">
561
+ All
562
+ </button>
563
+ <button id="clearAllFields" class="scope-action" type="button">
564
+ None
565
+ </button>
566
+ <button id="coreFields" class="scope-action" type="button">
567
+ Core
568
+ </button>
569
+ <button id="schemasOnlyFields" class="scope-action" type="button">
570
+ Schema
571
+ </button>
572
+ <button id="sourceOnlyFields" class="scope-action" type="button">
573
+ Source
574
+ </button>
522
575
  </div>
523
576
  <div id="fieldChipGroups" class="scope-groups"></div>
524
577
  </div>
@@ -549,15 +602,22 @@
549
602
  <div class="scope-group">
550
603
  <div class="scope-group-title">Views</div>
551
604
  <div class="scope-group-chips">
552
- <button id="tabLeaves" class="field-chip" type="button" aria-pressed="true">
605
+ <button
606
+ id="tabLeaves"
607
+ class="field-chip"
608
+ type="button"
609
+ aria-pressed="true"
610
+ >
553
611
  Leaves
554
612
  </button>
555
- <button id="tabChangelog" class="field-chip" type="button" aria-pressed="false">
613
+ <button
614
+ id="tabChangelog"
615
+ class="field-chip"
616
+ type="button"
617
+ aria-pressed="false"
618
+ >
556
619
  Changelog
557
620
  </button>
558
- <button id="tabChangelogSource" class="field-chip" type="button" aria-pressed="false">
559
- Changelog / By Source
560
- </button>
561
621
  </div>
562
622
  </div>
563
623
  </div>
@@ -567,7 +627,10 @@
567
627
  <div id="activeFilterChips" class="chips filters"></div>
568
628
  </div>
569
629
 
570
- <div id="status" class="meta">Load a leaves export, changelog export, or unified bundle JSON to begin.</div>
630
+ <div id="status" class="meta">
631
+ Load a leaves export, changelog export, or unified bundle JSON to
632
+ begin.
633
+ </div>
571
634
  </div>
572
635
 
573
636
  <div id="results"></div>
@@ -585,10 +648,22 @@
585
648
  label: 'description',
586
649
  get: (leaf) => [leaf.cfg?.description],
587
650
  },
588
- { id: 'docsGroup', label: 'docsGroup', get: (leaf) => [leaf.cfg?.docsGroup] },
651
+ {
652
+ id: 'docsGroup',
653
+ label: 'docsGroup',
654
+ get: (leaf) => [leaf.cfg?.docsGroup],
655
+ },
589
656
  { id: 'tags', label: 'tags', get: (leaf) => leaf.cfg?.tags || [] },
590
- { id: 'stability', label: 'stability', get: (leaf) => [leaf.cfg?.stability] },
591
- { id: 'docsMeta', label: 'docsMeta', get: (leaf) => [leaf.cfg?.docsMeta] },
657
+ {
658
+ id: 'stability',
659
+ label: 'stability',
660
+ get: (leaf) => [leaf.cfg?.stability],
661
+ },
662
+ {
663
+ id: 'docsMeta',
664
+ label: 'docsMeta',
665
+ get: (leaf) => [leaf.cfg?.docsMeta],
666
+ },
592
667
  {
593
668
  id: 'sourceDefinition',
594
669
  label: 'source definition',
@@ -617,7 +692,8 @@
617
692
  {
618
693
  id: 'types',
619
694
  label: 'types',
620
- get: (leaf, schemaFlatByLeaf) => schemaTypeTokens(schemaFlatByLeaf?.[leaf.key]),
695
+ get: (leaf, schemaFlatByLeaf) =>
696
+ schemaTypeTokens(schemaFlatByLeaf?.[leaf.key]),
621
697
  },
622
698
  {
623
699
  id: 'params',
@@ -694,35 +770,6 @@
694
770
  },
695
771
  ]
696
772
 
697
- const CHANGELOG_SOURCE_FIELDS = [
698
- {
699
- id: 'sourceObject',
700
- label: 'source object',
701
- get: (item) => [item.sourceId, item.displayName],
702
- },
703
- {
704
- id: 'eventType',
705
- label: 'event type',
706
- get: (item) => item.events.map((event) => event.type),
707
- },
708
- {
709
- id: 'impactedRoutes',
710
- label: 'impacted routes',
711
- get: (item) =>
712
- item.events.flatMap((event) => (Array.isArray(event.impactedRoutes) ? event.impactedRoutes : [])),
713
- },
714
- {
715
- id: 'schemaDiff',
716
- label: 'schema diff',
717
- get: (item) =>
718
- item.events.flatMap((event) =>
719
- Array.isArray(event.schemaDiff)
720
- ? event.schemaDiff.map((delta) => JSON.stringify(delta))
721
- : [],
722
- ),
723
- },
724
- ]
725
-
726
773
  const SCHEMA_SECTIONS = ['params', 'query', 'body', 'output']
727
774
  const SCHEMA_FIELD_IDS = new Set(SCHEMA_SECTIONS)
728
775
  const SOURCE_FIELD_IDS = new Set(['sourceDefinition', 'sourceSchemas'])
@@ -736,30 +783,29 @@
736
783
  {
737
784
  id: 'core',
738
785
  label: 'Core fields',
739
- fieldIds: SEARCH_FIELDS.filter((field) => CORE_FIELD_IDS.has(field.id)).map(
740
- (field) => field.id,
741
- ),
786
+ fieldIds: SEARCH_FIELDS.filter((field) =>
787
+ CORE_FIELD_IDS.has(field.id),
788
+ ).map((field) => field.id),
742
789
  },
743
790
  {
744
791
  id: 'schema',
745
792
  label: 'Schema fields',
746
- fieldIds: SEARCH_FIELDS.filter((field) => SCHEMA_SCOPE_FIELD_IDS.has(field.id)).map(
747
- (field) => field.id,
748
- ),
793
+ fieldIds: SEARCH_FIELDS.filter((field) =>
794
+ SCHEMA_SCOPE_FIELD_IDS.has(field.id),
795
+ ).map((field) => field.id),
749
796
  },
750
797
  {
751
798
  id: 'source',
752
799
  label: 'Source fields',
753
- fieldIds: SEARCH_FIELDS.filter((field) => SOURCE_FIELD_IDS.has(field.id)).map(
754
- (field) => field.id,
755
- ),
800
+ fieldIds: SEARCH_FIELDS.filter((field) =>
801
+ SOURCE_FIELD_IDS.has(field.id),
802
+ ).map((field) => field.id),
756
803
  },
757
804
  ]
758
805
 
759
806
  const VIEW_IDS = {
760
807
  leaves: 'leaves',
761
808
  changelog: 'changelog',
762
- changelogSource: 'changelogSource',
763
809
  }
764
810
 
765
811
  const VIEWER_MODE = {
@@ -790,11 +836,9 @@
790
836
  },
791
837
  [VIEW_IDS.changelog]: {
792
838
  fieldDefs: CHANGELOG_TIMELINE_FIELDS,
793
- defaultSelected: new Set(CHANGELOG_TIMELINE_FIELDS.map((field) => field.id)),
794
- },
795
- [VIEW_IDS.changelogSource]: {
796
- fieldDefs: CHANGELOG_SOURCE_FIELDS,
797
- defaultSelected: new Set(CHANGELOG_SOURCE_FIELDS.map((field) => field.id)),
839
+ defaultSelected: new Set(
840
+ CHANGELOG_TIMELINE_FIELDS.map((field) => field.id),
841
+ ),
798
842
  },
799
843
  }
800
844
 
@@ -807,9 +851,12 @@
807
851
  viewerMode: VIEWER_MODE.leaves,
808
852
  availableTabs: new Set([VIEW_IDS.leaves]),
809
853
  selectedFieldIdsByTab: {
810
- [VIEW_IDS.leaves]: new Set(TAB_CONFIG[VIEW_IDS.leaves].defaultSelected),
811
- [VIEW_IDS.changelog]: new Set(TAB_CONFIG[VIEW_IDS.changelog].defaultSelected),
812
- [VIEW_IDS.changelogSource]: new Set(TAB_CONFIG[VIEW_IDS.changelogSource].defaultSelected),
854
+ [VIEW_IDS.leaves]: new Set(
855
+ TAB_CONFIG[VIEW_IDS.leaves].defaultSelected,
856
+ ),
857
+ [VIEW_IDS.changelog]: new Set(
858
+ TAB_CONFIG[VIEW_IDS.changelog].defaultSelected,
859
+ ),
813
860
  },
814
861
  }
815
862
 
@@ -818,7 +865,9 @@
818
865
  const caseSensitiveToggle = document.getElementById('caseSensitive')
819
866
  const regexSearchToggle = document.getElementById('regexSearch')
820
867
  const typeMatchModeInput = document.getElementById('typeMatchMode')
821
- const schemaRowsMatchOnlyToggle = document.getElementById('schemaRowsMatchOnly')
868
+ const schemaRowsMatchOnlyToggle = document.getElementById(
869
+ 'schemaRowsMatchOnly',
870
+ )
822
871
  const fieldChipGroups = document.getElementById('fieldChipGroups')
823
872
  const activeFilterChips = document.getElementById('activeFilterChips')
824
873
  const selectAllFieldsBtn = document.getElementById('selectAllFields')
@@ -833,7 +882,6 @@
833
882
  const controlsEl = document.querySelector('.controls')
834
883
  const tabLeavesBtn = document.getElementById('tabLeaves')
835
884
  const tabChangelogBtn = document.getElementById('tabChangelog')
836
- const tabChangelogSourceBtn = document.getElementById('tabChangelogSource')
837
885
 
838
886
  const URL_PARAM_KEY = 'filters'
839
887
  let isHydratingFromUrl = false
@@ -846,7 +894,9 @@
846
894
  }
847
895
 
848
896
  function applyViewerModeMeta() {
849
- const meta = VIEWER_MODE_META[state.viewerMode] || VIEWER_MODE_META[VIEWER_MODE.leaves]
897
+ const meta =
898
+ VIEWER_MODE_META[state.viewerMode] ||
899
+ VIEWER_MODE_META[VIEWER_MODE.leaves]
850
900
  document.title = meta.title
851
901
  if (viewerHeadingEl) viewerHeadingEl.textContent = meta.heading
852
902
  }
@@ -958,7 +1008,8 @@
958
1008
  function toTokens(value) {
959
1009
  if (value === null || value === undefined) return []
960
1010
  if (typeof value === 'string') return [value]
961
- if (typeof value === 'number' || typeof value === 'boolean') return [String(value)]
1011
+ if (typeof value === 'number' || typeof value === 'boolean')
1012
+ return [String(value)]
962
1013
  if (Array.isArray(value)) return value.flatMap((item) => toTokens(item))
963
1014
  if (typeof value === 'object') return [JSON.stringify(value)]
964
1015
  return []
@@ -997,7 +1048,8 @@
997
1048
  function schemaSectionToSearchTokens(flatSchema, sectionName) {
998
1049
  if (!flatSchema || typeof flatSchema !== 'object') return []
999
1050
  const sectionEntries = Object.entries(flatSchema).filter(
1000
- ([path]) => path === sectionName || path.startsWith(`${sectionName}.`),
1051
+ ([path]) =>
1052
+ path === sectionName || path.startsWith(`${sectionName}.`),
1001
1053
  )
1002
1054
 
1003
1055
  if (sectionEntries.length === 0) return []
@@ -1045,12 +1097,20 @@
1045
1097
  return Array.from(tokens)
1046
1098
  }
1047
1099
 
1048
- function schemaEntryMatchesActiveFilter(sectionName, fullPath, info, engine, selectedIds) {
1100
+ function schemaEntryMatchesActiveFilter(
1101
+ sectionName,
1102
+ fullPath,
1103
+ info,
1104
+ engine,
1105
+ selectedIds,
1106
+ ) {
1049
1107
  if (!engine.active) return true
1050
1108
 
1051
1109
  let matched = false
1052
1110
  if (selectedIds.includes(sectionName)) {
1053
- matched = schemaEntryTokens(fullPath, info).some((token) => engine.test(token))
1111
+ matched = schemaEntryTokens(fullPath, info).some((token) =>
1112
+ engine.test(token),
1113
+ )
1054
1114
  }
1055
1115
 
1056
1116
  if (!matched && selectedIds.includes('types')) {
@@ -1064,7 +1124,9 @@
1064
1124
  }
1065
1125
 
1066
1126
  if (typeMatchModeInput.value === 'exact' && !engine.regex) {
1067
- const needle = engine.caseSensitive ? engine.query : engine.query.toLowerCase()
1127
+ const needle = engine.caseSensitive
1128
+ ? engine.query
1129
+ : engine.query.toLowerCase()
1068
1130
  const normalized = engine.caseSensitive
1069
1131
  ? typeTokens
1070
1132
  : typeTokens.map((token) => token.toLowerCase())
@@ -1086,8 +1148,14 @@
1086
1148
  return SEARCH_FIELDS.some((field) => {
1087
1149
  if (!selectedIds.includes(field.id)) return false
1088
1150
  const tokens = toTokens(field.get(leaf, schemaFlatByLeaf, payload))
1089
- if (field.id === 'types' && typeMatchModeInput.value === 'exact' && !engine.regex) {
1090
- const needle = engine.caseSensitive ? engine.query : engine.query.toLowerCase()
1151
+ if (
1152
+ field.id === 'types' &&
1153
+ typeMatchModeInput.value === 'exact' &&
1154
+ !engine.regex
1155
+ ) {
1156
+ const needle = engine.caseSensitive
1157
+ ? engine.query
1158
+ : engine.query.toLowerCase()
1091
1159
  const normalized = engine.caseSensitive
1092
1160
  ? tokens
1093
1161
  : tokens.map((token) => String(token).toLowerCase())
@@ -1125,14 +1193,25 @@
1125
1193
  }
1126
1194
 
1127
1195
  function setHighlighted(node, text, engine, selectedIds, fieldIds) {
1128
- node.innerHTML = highlightWithFieldScope(text, engine, selectedIds, fieldIds)
1196
+ node.innerHTML = highlightWithFieldScope(
1197
+ text,
1198
+ engine,
1199
+ selectedIds,
1200
+ fieldIds,
1201
+ )
1129
1202
  }
1130
1203
 
1131
1204
  function kv(key, value, engine, selectedIds, fieldIds) {
1132
1205
  const box = el('div', 'kv')
1133
1206
  box.appendChild(el('div', 'k', key))
1134
1207
  const valueNode = el('div', 'v mono')
1135
- setHighlighted(valueNode, value === undefined ? '—' : String(value), engine, selectedIds, fieldIds)
1208
+ setHighlighted(
1209
+ valueNode,
1210
+ value === undefined ? '—' : String(value),
1211
+ engine,
1212
+ selectedIds,
1213
+ fieldIds,
1214
+ )
1136
1215
  box.appendChild(valueNode)
1137
1216
  return box
1138
1217
  }
@@ -1197,7 +1276,12 @@
1197
1276
  link.href = href
1198
1277
  link.target = '_blank'
1199
1278
  link.rel = 'noopener noreferrer'
1200
- link.innerHTML = highlightWithFieldScope(label, engine, selectedIds, fieldIds)
1279
+ link.innerHTML = highlightWithFieldScope(
1280
+ label,
1281
+ engine,
1282
+ selectedIds,
1283
+ fieldIds,
1284
+ )
1201
1285
  labelNode.appendChild(link)
1202
1286
  box.appendChild(labelNode)
1203
1287
  return box
@@ -1233,7 +1317,11 @@
1233
1317
 
1234
1318
  function getSchemaSource(source, sectionName) {
1235
1319
  const schemaKey = SCHEMA_SOURCE_KEYS_BY_SECTION[sectionName]
1236
- if (!schemaKey || !source?.schemas || typeof source.schemas !== 'object') {
1320
+ if (
1321
+ !schemaKey ||
1322
+ !source?.schemas ||
1323
+ typeof source.schemas !== 'object'
1324
+ ) {
1237
1325
  return {
1238
1326
  schemaKey,
1239
1327
  sourceValue: undefined,
@@ -1247,8 +1335,11 @@
1247
1335
  }
1248
1336
 
1249
1337
  function resolveSchemaSourceLabel(schemaKey, sourceValue) {
1250
- if (!sourceValue || typeof sourceValue !== 'object') return schemaKey || 'schema'
1251
- return sourceValue.sourceName || sourceValue.tag || schemaKey || 'schema'
1338
+ if (!sourceValue || typeof sourceValue !== 'object')
1339
+ return schemaKey || 'schema'
1340
+ return (
1341
+ sourceValue.sourceName || sourceValue.tag || schemaKey || 'schema'
1342
+ )
1252
1343
  }
1253
1344
 
1254
1345
  function createTreeNode(name = '') {
@@ -1265,7 +1356,10 @@
1265
1356
  Object.entries(entries)
1266
1357
  .sort(([a], [b]) => a.localeCompare(b))
1267
1358
  .forEach(([fullPath, info]) => {
1268
- const trimmed = fullPath === sectionName ? '' : fullPath.slice(sectionName.length + 1)
1359
+ const trimmed =
1360
+ fullPath === sectionName
1361
+ ? ''
1362
+ : fullPath.slice(sectionName.length + 1)
1269
1363
  const segments = trimmed ? trimmed.split('.') : []
1270
1364
 
1271
1365
  let current = root
@@ -1307,10 +1401,13 @@
1307
1401
 
1308
1402
  const appendTypeCell = (row, info) => {
1309
1403
  const type = el('span', 'tree-pill')
1310
- setHighlighted(type, info?.type || info?.kind || '—', engine, selectedIds, [
1311
- sectionName,
1312
- 'types',
1313
- ])
1404
+ setHighlighted(
1405
+ type,
1406
+ info?.type || info?.kind || '',
1407
+ engine,
1408
+ selectedIds,
1409
+ [sectionName, 'types'],
1410
+ )
1314
1411
  row.appendChild(type)
1315
1412
  }
1316
1413
 
@@ -1331,7 +1428,13 @@
1331
1428
  .sort((a, b) => a.localeCompare(b))
1332
1429
  .forEach((key) =>
1333
1430
  container.appendChild(
1334
- renderTreeNode(node.children[key], engine, selectedIds, sectionName, false),
1431
+ renderTreeNode(
1432
+ node.children[key],
1433
+ engine,
1434
+ selectedIds,
1435
+ sectionName,
1436
+ false,
1437
+ ),
1335
1438
  ),
1336
1439
  )
1337
1440
  details.appendChild(container)
@@ -1352,7 +1455,8 @@
1352
1455
 
1353
1456
  const grouped = splitFlatSchemaBySection(flatSchema)
1354
1457
  let hasAnySchemaEntries = false
1355
- const limitToMatchedRows = isPressed(schemaRowsMatchOnlyToggle) && engine.active
1458
+ const limitToMatchedRows =
1459
+ isPressed(schemaRowsMatchOnlyToggle) && engine.active
1356
1460
 
1357
1461
  SCHEMA_SECTIONS.forEach((sectionName) => {
1358
1462
  const rawEntries = grouped[sectionName]
@@ -1361,7 +1465,13 @@
1361
1465
  const entries = limitToMatchedRows
1362
1466
  ? Object.fromEntries(
1363
1467
  Object.entries(rawEntries).filter(([fullPath, info]) =>
1364
- schemaEntryMatchesActiveFilter(sectionName, fullPath, info, engine, selectedIds),
1468
+ schemaEntryMatchesActiveFilter(
1469
+ sectionName,
1470
+ fullPath,
1471
+ info,
1472
+ engine,
1473
+ selectedIds,
1474
+ ),
1365
1475
  ),
1366
1476
  )
1367
1477
  : rawEntries
@@ -1390,7 +1500,9 @@
1390
1500
  }
1391
1501
 
1392
1502
  const tree = buildSchemaTree(entries, sectionName)
1393
- block.appendChild(renderTreeNode(tree, engine, selectedIds, sectionName, true))
1503
+ block.appendChild(
1504
+ renderTreeNode(tree, engine, selectedIds, sectionName, true),
1505
+ )
1394
1506
 
1395
1507
  section.appendChild(block)
1396
1508
  })
@@ -1417,7 +1529,9 @@
1417
1529
  const overview = el('div', 'section')
1418
1530
  overview.appendChild(el('h3', '', 'Overview'))
1419
1531
  const topGrid = el('div', 'grid-3')
1420
- topGrid.appendChild(kv('group', cfg.docsGroup, engine, selectedIds, 'docsGroup'))
1532
+ topGrid.appendChild(
1533
+ kv('group', cfg.docsGroup, engine, selectedIds, 'docsGroup'),
1534
+ )
1421
1535
  topGrid.appendChild(
1422
1536
  kv(
1423
1537
  'tags',
@@ -1427,10 +1541,22 @@
1427
1541
  'tags',
1428
1542
  ),
1429
1543
  )
1430
- topGrid.appendChild(kv('stability', cfg.stability, engine, selectedIds, 'stability'))
1544
+ topGrid.appendChild(
1545
+ kv('stability', cfg.stability, engine, selectedIds, 'stability'),
1546
+ )
1431
1547
  overview.appendChild(topGrid)
1432
- overview.appendChild(kv('summary', cfg.summary, engine, selectedIds, 'summary'))
1433
- overview.appendChild(kv('description', cfg.description, engine, selectedIds, 'description'))
1548
+ overview.appendChild(
1549
+ kv('summary', cfg.summary, engine, selectedIds, 'summary'),
1550
+ )
1551
+ overview.appendChild(
1552
+ kv(
1553
+ 'description',
1554
+ cfg.description,
1555
+ engine,
1556
+ selectedIds,
1557
+ 'description',
1558
+ ),
1559
+ )
1434
1560
 
1435
1561
  const iconRow = el('div', 'icon-row')
1436
1562
  if (cfg.feed) {
@@ -1473,11 +1599,22 @@
1473
1599
  const source = sourceByLeaf[leaf.key]
1474
1600
  if (source?.definition) {
1475
1601
  overview.appendChild(
1476
- createSourceRow('definition', source.definition, engine, selectedIds, 'sourceDefinition'),
1602
+ createSourceRow(
1603
+ 'definition',
1604
+ source.definition,
1605
+ engine,
1606
+ selectedIds,
1607
+ 'sourceDefinition',
1608
+ ),
1477
1609
  )
1478
1610
  }
1479
1611
 
1480
- const separatedSchemas = renderSeparatedSchemas(flatSchema, engine, selectedIds, source)
1612
+ const separatedSchemas = renderSeparatedSchemas(
1613
+ flatSchema,
1614
+ engine,
1615
+ selectedIds,
1616
+ source,
1617
+ )
1481
1618
  if (separatedSchemas) {
1482
1619
  content.appendChild(separatedSchemas)
1483
1620
  }
@@ -1485,7 +1622,21 @@
1485
1622
  const routeHistory = state.changelogPayload?.byRoute?.[leaf.key]
1486
1623
  if (Array.isArray(routeHistory) && routeHistory.length > 0) {
1487
1624
  const commitBySha = new Map(
1488
- (state.changelogPayload?.commits || []).map((commit) => [commit.sha, commit]),
1625
+ (state.changelogPayload?.commits || []).map((commit) => [
1626
+ commit.sha,
1627
+ commit,
1628
+ ]),
1629
+ )
1630
+ const commitOrder = new Map(
1631
+ (state.changelogPayload?.commits || []).map((commit, index) => [
1632
+ commit.sha,
1633
+ index,
1634
+ ]),
1635
+ )
1636
+ const routeHistoryNewestFirst = sortEventsNewestFirst(
1637
+ routeHistory,
1638
+ commitBySha,
1639
+ commitOrder,
1489
1640
  )
1490
1641
  const routeChangelogSection = el('div', 'section')
1491
1642
  const routeChangelogDetails = el('details')
@@ -1500,7 +1651,7 @@
1500
1651
  routeChangelogDetails.appendChild(routeChangelogSummary)
1501
1652
 
1502
1653
  const routeChangelogBody = el('div', 'leaf-content')
1503
- routeHistory.forEach((event) => {
1654
+ routeHistoryNewestFirst.forEach((event) => {
1504
1655
  routeChangelogBody.appendChild(renderEventCard(event, commitBySha))
1505
1656
  })
1506
1657
  routeChangelogDetails.appendChild(routeChangelogBody)
@@ -1553,18 +1704,134 @@
1553
1704
  return parts.join(', ')
1554
1705
  }
1555
1706
 
1707
+ function schemaFlags(nullable, optional) {
1708
+ return `${nullable ? '-' : ''}${optional ? '?' : ''}`
1709
+ }
1710
+
1711
+ function formatEntryTypeLabel(entry) {
1712
+ if (!entry || typeof entry !== 'object') return null
1713
+ const type = typeof entry.type === 'string' ? entry.type : 'unknown'
1714
+ return `${type}${schemaFlags(Boolean(entry.nullable), Boolean(entry.optional))}`
1715
+ }
1716
+
1717
+ function formatPathLabel(path, entry) {
1718
+ const basePath = typeof path === 'string' ? path : ''
1719
+ return `${basePath}${schemaFlags(Boolean(entry?.nullable), Boolean(entry?.optional))}`
1720
+ }
1721
+
1722
+ function parseSignatureTypeLabel(signature) {
1723
+ if (typeof signature !== 'string') return null
1724
+ const typeMatch = signature.match(/(?:^|,\s*)type=([^,]+)/)
1725
+ const nullableMatch = signature.match(/(?:^|,\s*)nullable=(true|false)/)
1726
+ const optionalMatch = signature.match(/(?:^|,\s*)optional=(true|false)/)
1727
+ if (!typeMatch) return null
1728
+ const type = typeMatch[1]
1729
+ const nullable = nullableMatch?.[1] === 'true'
1730
+ const optional = optionalMatch?.[1] === 'true'
1731
+ return `${type}${schemaFlags(nullable, optional)}`
1732
+ }
1733
+
1734
+ function formatSourceTypeLabel(signatures) {
1735
+ if (!Array.isArray(signatures) || signatures.length === 0) return null
1736
+ const labels = signatures
1737
+ .map((item) => parseSignatureTypeLabel(item))
1738
+ .filter(Boolean)
1739
+ if (labels.length === 0) return null
1740
+ return labels.join(' | ')
1741
+ }
1742
+
1743
+ function schemaDiffOperation(op) {
1744
+ if (op === 'added') {
1745
+ return { symbol: '+', word: 'added', className: 'added' }
1746
+ }
1747
+ if (op === 'removed') {
1748
+ return { symbol: '-', word: 'removed', className: 'removed' }
1749
+ }
1750
+ return { symbol: '*', word: 'changed', className: 'changed' }
1751
+ }
1752
+
1753
+ function buildSchemaDiffLine(op, beforeType, pathLabel, afterType) {
1754
+ if (!pathLabel) return null
1755
+ const line = el('div', 'meta mono changelog-diff-line')
1756
+ const operation = schemaDiffOperation(op)
1757
+ line.appendChild(
1758
+ el(
1759
+ 'span',
1760
+ `changelog-op ${operation.className}`,
1761
+ `${operation.symbol} ${operation.word}`,
1762
+ ),
1763
+ )
1764
+ if (beforeType) {
1765
+ line.appendChild(el('span', 'changelog-schema-type', beforeType))
1766
+ }
1767
+ line.appendChild(el('span', '', pathLabel))
1768
+ if (operation.className === 'changed' && afterType) {
1769
+ line.appendChild(el('span', '', '->'))
1770
+ line.appendChild(el('span', 'changelog-schema-type', afterType))
1771
+ }
1772
+ return line
1773
+ }
1774
+
1775
+ function buildLegacySchemaDiffLine(text) {
1776
+ return el('div', 'meta mono', text)
1777
+ }
1778
+
1556
1779
  function formatSchemaDiffLine(delta) {
1557
1780
  if (!delta || typeof delta !== 'object') return null
1558
1781
  if ('section' in delta) {
1559
1782
  const location = `${delta.section}.${delta.path}`
1560
- if (delta.op === 'added') return `added ${location}: ${JSON.stringify(delta.after || [])}`
1561
- if (delta.op === 'removed') return `removed ${location}: ${JSON.stringify(delta.before || [])}`
1562
- return `changed ${location}: ${JSON.stringify(delta.before || [])} -> ${JSON.stringify(delta.after || [])}`
1783
+ if (delta.op === 'added') {
1784
+ const afterType = formatSourceTypeLabel(delta.after)
1785
+ if (afterType) return buildSchemaDiffLine(delta.op, afterType, location)
1786
+ return buildLegacySchemaDiffLine(
1787
+ `added ${location}: ${JSON.stringify(delta.after || [])}`,
1788
+ )
1789
+ }
1790
+ if (delta.op === 'removed') {
1791
+ const beforeType = formatSourceTypeLabel(delta.before)
1792
+ if (beforeType)
1793
+ return buildSchemaDiffLine(delta.op, beforeType, location)
1794
+ return buildLegacySchemaDiffLine(
1795
+ `removed ${location}: ${JSON.stringify(delta.before || [])}`,
1796
+ )
1797
+ }
1798
+ const beforeType = formatSourceTypeLabel(delta.before)
1799
+ const afterType = formatSourceTypeLabel(delta.after)
1800
+ if (beforeType || afterType) {
1801
+ return buildSchemaDiffLine(
1802
+ delta.op,
1803
+ beforeType || 'unknown',
1804
+ location,
1805
+ afterType || 'unknown',
1806
+ )
1807
+ }
1808
+ return buildLegacySchemaDiffLine(
1809
+ `changed ${location}: ${JSON.stringify(delta.before || [])} -> ${JSON.stringify(delta.after || [])}`,
1810
+ )
1563
1811
  }
1564
1812
  if ('path' in delta) {
1565
- if (delta.op === 'added') return `added ${delta.path}: ${formatRouteSchemaEntry(delta.after)}`
1566
- if (delta.op === 'removed') return `removed ${delta.path}: ${formatRouteSchemaEntry(delta.before)}`
1567
- return `changed ${delta.path}: ${formatRouteSchemaEntry(delta.before)} -> ${formatRouteSchemaEntry(delta.after)}`
1813
+ if (delta.op === 'added') {
1814
+ const afterType = formatEntryTypeLabel(delta.after)
1815
+ const pathLabel = formatPathLabel(delta.path, delta.after)
1816
+ if (afterType) return buildSchemaDiffLine(delta.op, afterType, pathLabel)
1817
+ return buildLegacySchemaDiffLine(
1818
+ `added ${delta.path}: ${formatRouteSchemaEntry(delta.after)}`,
1819
+ )
1820
+ }
1821
+ if (delta.op === 'removed') {
1822
+ const beforeType = formatEntryTypeLabel(delta.before)
1823
+ const pathLabel = formatPathLabel(delta.path, delta.before)
1824
+ if (beforeType)
1825
+ return buildSchemaDiffLine(delta.op, beforeType, pathLabel)
1826
+ return buildLegacySchemaDiffLine(
1827
+ `removed ${delta.path}: ${formatRouteSchemaEntry(delta.before)}`,
1828
+ )
1829
+ }
1830
+ const beforeType = formatEntryTypeLabel(delta.before) || 'unknown'
1831
+ const afterType = formatEntryTypeLabel(delta.after) || 'unknown'
1832
+ const pathLabel =
1833
+ formatPathLabel(delta.path, delta.after || delta.before) || delta.path
1834
+ return buildSchemaDiffLine(delta.op, beforeType, pathLabel, afterType)
1568
1835
  }
1569
1836
  return null
1570
1837
  }
@@ -1589,17 +1856,21 @@
1589
1856
  card.appendChild(el('div', 'meta', commitMeta))
1590
1857
  card.appendChild(el('div', 'mono', event.type))
1591
1858
  if (Array.isArray(event.schemaDiff) && event.schemaDiff.length > 0) {
1592
- card.appendChild(el('div', 'meta', `schema changes: ${event.schemaDiff.length}`))
1859
+ card.appendChild(
1860
+ el('div', 'meta', `schema changes: ${event.schemaDiff.length}`),
1861
+ )
1593
1862
  const schemaChanges = el('div')
1594
1863
  event.schemaDiff.forEach((delta) => {
1595
- const line = formatSchemaDiffLine(delta)
1596
- if (!line) return
1597
- schemaChanges.appendChild(el('div', 'meta', line))
1864
+ const lineNode = formatSchemaDiffLine(delta)
1865
+ if (!lineNode) return
1866
+ schemaChanges.appendChild(lineNode)
1598
1867
  })
1599
1868
  card.appendChild(schemaChanges)
1600
1869
  }
1601
1870
  if (Array.isArray(event.cfgDiff) && event.cfgDiff.length > 0) {
1602
- card.appendChild(el('div', 'meta', `cfg changes: ${event.cfgDiff.length}`))
1871
+ card.appendChild(
1872
+ el('div', 'meta', `cfg changes: ${event.cfgDiff.length}`),
1873
+ )
1603
1874
  const cfgChanges = el('div')
1604
1875
  event.cfgDiff.forEach((diff) => {
1605
1876
  const line = formatCfgDiffLine(diff)
@@ -1608,8 +1879,17 @@
1608
1879
  })
1609
1880
  card.appendChild(cfgChanges)
1610
1881
  }
1611
- if (Array.isArray(event.impactedRoutes) && event.impactedRoutes.length > 0) {
1612
- card.appendChild(el('div', 'meta', `impacted routes: ${event.impactedRoutes.length}`))
1882
+ if (
1883
+ Array.isArray(event.impactedRoutes) &&
1884
+ event.impactedRoutes.length > 0
1885
+ ) {
1886
+ card.appendChild(
1887
+ el(
1888
+ 'div',
1889
+ 'meta',
1890
+ `impacted routes: ${event.impactedRoutes.length}`,
1891
+ ),
1892
+ )
1613
1893
  }
1614
1894
  return card
1615
1895
  }
@@ -1629,14 +1909,16 @@
1629
1909
 
1630
1910
  function renderChangelogEventLines(event, commitBySha, contextLabel) {
1631
1911
  const row = el('div', 'changelog-line-item')
1632
- row.appendChild(el('div', 'meta', buildCommitMetaLine(event, commitBySha)))
1912
+ row.appendChild(
1913
+ el('div', 'meta', buildCommitMetaLine(event, commitBySha)),
1914
+ )
1633
1915
  row.appendChild(el('div', 'mono', `${contextLabel} (${event.type})`))
1634
1916
 
1635
1917
  if (Array.isArray(event.schemaDiff) && event.schemaDiff.length > 0) {
1636
1918
  event.schemaDiff.forEach((delta) => {
1637
- const line = formatSchemaDiffLine(delta)
1638
- if (!line) return
1639
- row.appendChild(el('div', 'meta mono', line))
1919
+ const lineNode = formatSchemaDiffLine(delta)
1920
+ if (!lineNode) return
1921
+ row.appendChild(lineNode)
1640
1922
  })
1641
1923
  }
1642
1924
  if (Array.isArray(event.cfgDiff) && event.cfgDiff.length > 0) {
@@ -1646,19 +1928,57 @@
1646
1928
  row.appendChild(el('div', 'meta mono', line))
1647
1929
  })
1648
1930
  }
1649
- if (Array.isArray(event.impactedRoutes) && event.impactedRoutes.length > 0) {
1931
+ if (
1932
+ Array.isArray(event.impactedRoutes) &&
1933
+ event.impactedRoutes.length > 0
1934
+ ) {
1650
1935
  row.appendChild(
1651
- el('div', 'meta mono', `impacted routes: ${event.impactedRoutes.join(', ')}`),
1936
+ el(
1937
+ 'div',
1938
+ 'meta mono',
1939
+ `impacted routes: ${event.impactedRoutes.join(', ')}`,
1940
+ ),
1652
1941
  )
1653
1942
  }
1654
1943
 
1655
1944
  return row
1656
1945
  }
1657
1946
 
1947
+ function commitTimestamp(commit) {
1948
+ if (!commit?.authorDate) return Number.NEGATIVE_INFINITY
1949
+ const parsed = Date.parse(commit.authorDate)
1950
+ return Number.isNaN(parsed) ? Number.NEGATIVE_INFINITY : parsed
1951
+ }
1952
+
1953
+ function compareEventsNewestFirst(a, b, commitBySha, commitOrder) {
1954
+ const commitA = commitBySha.get(a?.commitSha)
1955
+ const commitB = commitBySha.get(b?.commitSha)
1956
+ const timeDelta = commitTimestamp(commitB) - commitTimestamp(commitA)
1957
+ if (timeDelta !== 0) return timeDelta
1958
+
1959
+ const indexA = commitOrder.get(a?.commitSha) ?? -1
1960
+ const indexB = commitOrder.get(b?.commitSha) ?? -1
1961
+ if (indexA !== indexB) return indexB - indexA
1962
+
1963
+ return String(b?.commitSha || '').localeCompare(
1964
+ String(a?.commitSha || ''),
1965
+ )
1966
+ }
1967
+
1968
+ function sortEventsNewestFirst(events, commitBySha, commitOrder) {
1969
+ return [...(events || [])].sort((a, b) =>
1970
+ compareEventsNewestFirst(a, b, commitBySha, commitOrder),
1971
+ )
1972
+ }
1973
+
1658
1974
  function buildChronologicalEvents(payload) {
1659
1975
  if (!payload) return []
1660
- const commitBySha = new Map((payload.commits || []).map((commit) => [commit.sha, commit]))
1661
- const commitOrder = new Map((payload.commits || []).map((commit, index) => [commit.sha, index]))
1976
+ const commitBySha = new Map(
1977
+ (payload.commits || []).map((commit) => [commit.sha, commit]),
1978
+ )
1979
+ const commitOrder = new Map(
1980
+ (payload.commits || []).map((commit, index) => [commit.sha, index]),
1981
+ )
1662
1982
  const rows = []
1663
1983
  let sequence = 0
1664
1984
 
@@ -1669,28 +1989,34 @@
1669
1989
  routeKey,
1670
1990
  event,
1671
1991
  commit: commitBySha.get(event.commitSha),
1992
+ commitSort: commitTimestamp(commitBySha.get(event.commitSha)),
1672
1993
  commitIndex: commitOrder.get(event.commitSha) ?? -1,
1673
1994
  eventIndex: sequence++,
1674
1995
  })
1675
1996
  })
1676
1997
  })
1677
1998
 
1678
- Object.entries(payload.bySourceObject || {}).forEach(([sourceObjectId, events]) => {
1679
- ;(events || []).forEach((event) => {
1680
- rows.push({
1681
- eventKind: 'source',
1682
- sourceObjectId,
1683
- displayName: event.displayName || sourceObjectId,
1684
- event,
1685
- commit: commitBySha.get(event.commitSha),
1686
- commitIndex: commitOrder.get(event.commitSha) ?? -1,
1687
- eventIndex: sequence++,
1999
+ Object.entries(payload.bySourceObject || {}).forEach(
2000
+ ([sourceObjectId, events]) => {
2001
+ ;(events || []).forEach((event) => {
2002
+ rows.push({
2003
+ eventKind: 'source',
2004
+ sourceObjectId,
2005
+ displayName: event.displayName || sourceObjectId,
2006
+ event,
2007
+ commit: commitBySha.get(event.commitSha),
2008
+ commitSort: commitTimestamp(commitBySha.get(event.commitSha)),
2009
+ commitIndex: commitOrder.get(event.commitSha) ?? -1,
2010
+ eventIndex: sequence++,
2011
+ })
1688
2012
  })
1689
- })
1690
- })
2013
+ },
2014
+ )
1691
2015
 
1692
2016
  rows.sort((a, b) => {
1693
- if (a.commitIndex !== b.commitIndex) return b.commitIndex - a.commitIndex
2017
+ if (a.commitSort !== b.commitSort) return b.commitSort - a.commitSort
2018
+ if (a.commitIndex !== b.commitIndex)
2019
+ return b.commitIndex - a.commitIndex
1694
2020
  return b.eventIndex - a.eventIndex
1695
2021
  })
1696
2022
 
@@ -1703,43 +2029,14 @@
1703
2029
  if (tabId === VIEW_IDS.changelog) {
1704
2030
  return buildChronologicalEvents(payload)
1705
2031
  }
1706
- if (tabId === VIEW_IDS.changelogSource) {
1707
- const commitOrder = new Map((payload.commits || []).map((commit, index) => [commit.sha, index]))
1708
- const sortEventsNewestFirst = (events) =>
1709
- [...(events || [])].sort((a, b) => {
1710
- const aIndex = commitOrder.get(a?.commitSha) ?? -1
1711
- const bIndex = commitOrder.get(b?.commitSha) ?? -1
1712
- if (aIndex !== bIndex) return bIndex - aIndex
1713
- return String(b?.commitSha || '').localeCompare(String(a?.commitSha || ''))
1714
- })
1715
-
1716
- return Object.entries(payload.bySourceObject || {})
1717
- .map(([sourceId, events]) => {
1718
- const sortedEvents = sortEventsNewestFirst(events)
1719
- const latestCommitIndex = sortedEvents.length
1720
- ? commitOrder.get(sortedEvents[0]?.commitSha) ?? -1
1721
- : -1
1722
- return {
1723
- sourceId,
1724
- events: sortedEvents,
1725
- displayName: sortedEvents[0]?.displayName || sourceId,
1726
- latestCommitIndex,
1727
- }
1728
- })
1729
- .sort((a, b) => {
1730
- if (a.latestCommitIndex !== b.latestCommitIndex) {
1731
- return b.latestCommitIndex - a.latestCommitIndex
1732
- }
1733
- return a.displayName.localeCompare(b.displayName)
1734
- })
1735
- }
1736
2032
  return []
1737
2033
  }
1738
2034
 
1739
2035
  function matchesGenericItem(item, fieldDefs, engine, selectedIds) {
1740
2036
  if (!engine.active) return true
1741
2037
  return fieldDefs.some((field) => {
1742
- if (selectedIds.length > 0 && !selectedIds.includes(field.id)) return false
2038
+ if (selectedIds.length > 0 && !selectedIds.includes(field.id))
2039
+ return false
1743
2040
  const tokens = toTokens(field.get(item))
1744
2041
  return tokens.some((token) => engine.test(token))
1745
2042
  })
@@ -1758,10 +2055,14 @@
1758
2055
  function renderTabButtons() {
1759
2056
  setPressed(tabLeavesBtn, state.activeTab === VIEW_IDS.leaves)
1760
2057
  setPressed(tabChangelogBtn, state.activeTab === VIEW_IDS.changelog)
1761
- setPressed(tabChangelogSourceBtn, state.activeTab === VIEW_IDS.changelogSource)
1762
- tabLeavesBtn.style.display = state.availableTabs.has(VIEW_IDS.leaves) ? '' : 'none'
1763
- tabChangelogBtn.style.display = state.availableTabs.has(VIEW_IDS.changelog) ? '' : 'none'
1764
- tabChangelogSourceBtn.style.display = state.availableTabs.has(VIEW_IDS.changelogSource) ? '' : 'none'
2058
+ tabLeavesBtn.style.display = state.availableTabs.has(VIEW_IDS.leaves)
2059
+ ? ''
2060
+ : 'none'
2061
+ tabChangelogBtn.style.display = state.availableTabs.has(
2062
+ VIEW_IDS.changelog,
2063
+ )
2064
+ ? ''
2065
+ : 'none'
1765
2066
  const isLeavesTab = state.activeTab === VIEW_IDS.leaves
1766
2067
  coreFieldsBtn.style.display = isLeavesTab ? '' : 'none'
1767
2068
  schemasOnlyFieldsBtn.style.display = isLeavesTab ? '' : 'none'
@@ -1777,7 +2078,9 @@
1777
2078
  if (engine.error) {
1778
2079
  statusEl.textContent = `Invalid regex: ${engine.error}`
1779
2080
  resultsEl.innerHTML = ''
1780
- resultsEl.appendChild(el('div', 'empty', 'Fix the regex to continue.'))
2081
+ resultsEl.appendChild(
2082
+ el('div', 'empty', 'Fix the regex to continue.'),
2083
+ )
1781
2084
  renderActiveFilterChips({
1782
2085
  selectedIds: selectedFieldIds(),
1783
2086
  hasRegexError: true,
@@ -1790,43 +2093,41 @@
1790
2093
  renderTabButtons()
1791
2094
  renderActiveFilterChips({ selectedIds, hasRegexError: false })
1792
2095
  if (state.activeTab === VIEW_IDS.leaves) {
1793
- const filtered = state.leaves.filter((leaf) => matchesLeaf(leaf, engine, selectedIds))
2096
+ const filtered = state.leaves.filter((leaf) =>
2097
+ matchesLeaf(leaf, engine, selectedIds),
2098
+ )
1794
2099
  statusEl.textContent = `${filtered.length} / ${state.leaves.length} routes matched.`
1795
- renderCollection(filtered, (leaf) => renderLeaf(leaf, engine, selectedIds), 'No matches.')
2100
+ renderCollection(
2101
+ filtered,
2102
+ (leaf) => renderLeaf(leaf, engine, selectedIds),
2103
+ 'No matches.',
2104
+ )
1796
2105
  } else {
1797
2106
  const fieldDefs = currentFieldDefs()
1798
2107
  const items = changelogItemsByTab(state.activeTab)
1799
- const filtered = items.filter((item) => matchesGenericItem(item, fieldDefs, engine, selectedIds))
1800
- statusEl.textContent =
1801
- state.activeTab === VIEW_IDS.changelog
1802
- ? `${filtered.length} / ${items.length} changelog events matched.`
1803
- : `${filtered.length} / ${items.length} source changelog groups matched.`
2108
+ const filtered = items.filter((item) =>
2109
+ matchesGenericItem(item, fieldDefs, engine, selectedIds),
2110
+ )
2111
+ statusEl.textContent = `${filtered.length} / ${items.length} changelog events matched.`
1804
2112
  const commitBySha = new Map(
1805
- (state.changelogPayload?.commits || []).map((commit) => [commit.sha, commit]),
2113
+ (state.changelogPayload?.commits || []).map((commit) => [
2114
+ commit.sha,
2115
+ commit,
2116
+ ]),
1806
2117
  )
1807
2118
  if (state.viewerMode === VIEWER_MODE.changelog) {
1808
2119
  renderCollection(
1809
2120
  filtered,
1810
2121
  (item) => {
1811
- if (state.activeTab === VIEW_IDS.changelog) {
1812
- const contextLabel =
1813
- item.eventKind === 'route'
1814
- ? `Route: ${item.routeKey}`
1815
- : `Source: ${item.displayName || item.sourceObjectId}`
1816
- return renderChangelogEventLines(item.event, commitBySha, contextLabel)
1817
- }
1818
-
1819
- const lines = el('div', 'changelog-lines')
1820
- ;(item.events || []).forEach((event) => {
1821
- lines.appendChild(
1822
- renderChangelogEventLines(
1823
- event,
1824
- commitBySha,
1825
- `Source: ${item.displayName || item.sourceId}`,
1826
- ),
1827
- )
1828
- })
1829
- return lines
2122
+ const contextLabel =
2123
+ item.eventKind === 'route'
2124
+ ? `Route: ${item.routeKey}`
2125
+ : `Source: ${item.displayName || item.sourceObjectId}`
2126
+ return renderChangelogEventLines(
2127
+ item.event,
2128
+ commitBySha,
2129
+ contextLabel,
2130
+ )
1830
2131
  },
1831
2132
  'No matches.',
1832
2133
  )
@@ -1835,16 +2136,9 @@
1835
2136
  filtered,
1836
2137
  (item) => {
1837
2138
  const details = el('details', 'leaf')
1838
- const title =
1839
- state.activeTab === VIEW_IDS.changelog
1840
- ? `${item.eventKind === 'route' ? `Route: ${item.routeKey}` : `Source: ${item.displayName || item.sourceObjectId}`} (${item.event.type})`
1841
- : `${item.displayName} (${item.events.length})`
2139
+ const title = `${item.eventKind === 'route' ? `Route: ${item.routeKey}` : `Source: ${item.displayName || item.sourceObjectId}`} (${item.event.type})`
1842
2140
  details.appendChild(el('summary', '', title))
1843
- if (state.activeTab === VIEW_IDS.changelog) {
1844
- details.appendChild(renderEventCard(item.event, commitBySha))
1845
- } else {
1846
- item.events.forEach((event) => details.appendChild(renderEventCard(event, commitBySha)))
1847
- }
2141
+ details.appendChild(renderEventCard(item.event, commitBySha))
1848
2142
  return details
1849
2143
  },
1850
2144
  'No matches.',
@@ -1855,15 +2149,17 @@
1855
2149
  }
1856
2150
 
1857
2151
  function selectedFieldIds() {
1858
- const selected = state.selectedFieldIdsByTab[state.activeTab] || new Set()
1859
- return currentFieldDefs().filter((field) => selected.has(field.id)).map(
1860
- (field) => field.id,
1861
- )
2152
+ const selected =
2153
+ state.selectedFieldIdsByTab[state.activeTab] || new Set()
2154
+ return currentFieldDefs()
2155
+ .filter((field) => selected.has(field.id))
2156
+ .map((field) => field.id)
1862
2157
  }
1863
2158
 
1864
2159
  function renderFieldChips() {
1865
2160
  const fieldDefs = currentFieldDefs()
1866
- const selected = state.selectedFieldIdsByTab[state.activeTab] || new Set()
2161
+ const selected =
2162
+ state.selectedFieldIdsByTab[state.activeTab] || new Set()
1867
2163
  fieldChipGroups.innerHTML = ''
1868
2164
  const groups =
1869
2165
  state.activeTab === VIEW_IDS.leaves
@@ -1871,7 +2167,10 @@
1871
2167
  : [
1872
2168
  {
1873
2169
  id: 'tabFields',
1874
- label: state.activeTab === VIEW_IDS.changelog ? 'Changelog fields' : 'Source fields',
2170
+ label:
2171
+ state.activeTab === VIEW_IDS.changelog
2172
+ ? 'Changelog fields'
2173
+ : 'Source fields',
1875
2174
  fieldIds: fieldDefs.map((field) => field.id),
1876
2175
  },
1877
2176
  ]
@@ -1910,7 +2209,9 @@
1910
2209
  function setFieldSelection(allowedIds, options = {}) {
1911
2210
  const { rerender = true } = options
1912
2211
  state.selectedFieldIdsByTab[state.activeTab] = new Set(
1913
- currentFieldDefs().map((field) => field.id).filter((id) => allowedIds.has(id)),
2212
+ currentFieldDefs()
2213
+ .map((field) => field.id)
2214
+ .filter((id) => allowedIds.has(id)),
1914
2215
  )
1915
2216
  renderFieldChips()
1916
2217
  if (rerender) renderResults()
@@ -1922,7 +2223,10 @@
1922
2223
  setPressed(regexSearchToggle, false)
1923
2224
  typeMatchModeInput.value = 'contains'
1924
2225
  setPressed(schemaRowsMatchOnlyToggle, false)
1925
- setFieldSelection(new Set(currentFieldDefs().map((field) => field.id)), { rerender: false })
2226
+ setFieldSelection(
2227
+ new Set(currentFieldDefs().map((field) => field.id)),
2228
+ { rerender: false },
2229
+ )
1926
2230
  renderResults()
1927
2231
  }
1928
2232
 
@@ -1987,12 +2291,16 @@
1987
2291
  }
1988
2292
 
1989
2293
  if (chipModels.length === 0) {
1990
- activeFilterChips.appendChild(el('span', 'empty', 'No active filters'))
2294
+ activeFilterChips.appendChild(
2295
+ el('span', 'empty', 'No active filters'),
2296
+ )
1991
2297
  return
1992
2298
  }
1993
2299
 
1994
2300
  chipModels.forEach((chipModel) => {
1995
- const chip = document.createElement(chipModel.onClick ? 'button' : 'span')
2301
+ const chip = document.createElement(
2302
+ chipModel.onClick ? 'button' : 'span',
2303
+ )
1996
2304
  chip.className = chipModel.onClick ? 'chip chip-btn' : 'chip'
1997
2305
  chip.textContent = chipModel.text
1998
2306
  if (chipModel.onClick) {
@@ -2009,10 +2317,14 @@
2009
2317
  search: searchInput.value,
2010
2318
  caseSensitive: isPressed(caseSensitiveToggle),
2011
2319
  regex: isPressed(regexSearchToggle),
2012
- typeMatchMode: typeMatchModeInput.value === 'exact' ? 'exact' : 'contains',
2320
+ typeMatchMode:
2321
+ typeMatchModeInput.value === 'exact' ? 'exact' : 'contains',
2013
2322
  schemaRowsMatchOnly: isPressed(schemaRowsMatchOnlyToggle),
2014
2323
  selectedFieldsByTab: Object.fromEntries(
2015
- Object.entries(state.selectedFieldIdsByTab).map(([key, value]) => [key, Array.from(value)]),
2324
+ Object.entries(state.selectedFieldIdsByTab).map(([key, value]) => [
2325
+ key,
2326
+ Array.from(value),
2327
+ ]),
2016
2328
  ),
2017
2329
  }
2018
2330
  }
@@ -2021,7 +2333,9 @@
2021
2333
  if (isHydratingFromUrl) return
2022
2334
 
2023
2335
  const data = collectFilterState()
2024
- const params = new URLSearchParams(window.location.hash.replace(/^#/, ''))
2336
+ const params = new URLSearchParams(
2337
+ window.location.hash.replace(/^#/, ''),
2338
+ )
2025
2339
  const serialized = encodeURIComponent(JSON.stringify(data))
2026
2340
  params.set(URL_PARAM_KEY, serialized)
2027
2341
  const nextHash = params.toString()
@@ -2031,7 +2345,9 @@
2031
2345
  }
2032
2346
 
2033
2347
  function hydrateFilterStateFromUrl() {
2034
- const params = new URLSearchParams(window.location.hash.replace(/^#/, ''))
2348
+ const params = new URLSearchParams(
2349
+ window.location.hash.replace(/^#/, ''),
2350
+ )
2035
2351
  const raw = params.get(URL_PARAM_KEY)
2036
2352
  if (!raw) return
2037
2353
 
@@ -2040,28 +2356,39 @@
2040
2356
  isHydratingFromUrl = true
2041
2357
  if (
2042
2358
  parsed.activeTab === VIEW_IDS.leaves ||
2043
- parsed.activeTab === VIEW_IDS.changelog ||
2044
- parsed.activeTab === VIEW_IDS.changelogSource
2359
+ parsed.activeTab === VIEW_IDS.changelog
2045
2360
  ) {
2046
2361
  state.activeTab = parsed.activeTab
2047
2362
  }
2048
2363
  if (parsed.activeTab === 'changelogRoute') {
2049
2364
  state.activeTab = VIEW_IDS.changelog
2050
2365
  }
2051
- if (typeof parsed.search === 'string') searchInput.value = parsed.search
2366
+ if (typeof parsed.search === 'string')
2367
+ searchInput.value = parsed.search
2052
2368
  setPressed(caseSensitiveToggle, Boolean(parsed.caseSensitive))
2053
2369
  setPressed(regexSearchToggle, Boolean(parsed.regex))
2054
- if (parsed.typeMatchMode === 'exact' || parsed.typeMatchMode === 'contains') {
2370
+ if (
2371
+ parsed.typeMatchMode === 'exact' ||
2372
+ parsed.typeMatchMode === 'contains'
2373
+ ) {
2055
2374
  typeMatchModeInput.value = parsed.typeMatchMode
2056
2375
  }
2057
- setPressed(schemaRowsMatchOnlyToggle, Boolean(parsed.schemaRowsMatchOnly))
2376
+ setPressed(
2377
+ schemaRowsMatchOnlyToggle,
2378
+ Boolean(parsed.schemaRowsMatchOnly),
2379
+ )
2058
2380
 
2059
- if (parsed.selectedFieldsByTab && typeof parsed.selectedFieldsByTab === 'object') {
2060
- Object.entries(parsed.selectedFieldsByTab).forEach(([tabId, ids]) => {
2061
- if (!Array.isArray(ids)) return
2062
- if (!state.selectedFieldIdsByTab[tabId]) return
2063
- state.selectedFieldIdsByTab[tabId] = new Set(ids)
2064
- })
2381
+ if (
2382
+ parsed.selectedFieldsByTab &&
2383
+ typeof parsed.selectedFieldsByTab === 'object'
2384
+ ) {
2385
+ Object.entries(parsed.selectedFieldsByTab).forEach(
2386
+ ([tabId, ids]) => {
2387
+ if (!Array.isArray(ids)) return
2388
+ if (!state.selectedFieldIdsByTab[tabId]) return
2389
+ state.selectedFieldIdsByTab[tabId] = new Set(ids)
2390
+ },
2391
+ )
2065
2392
  }
2066
2393
  } catch (error) {
2067
2394
  // Ignore malformed hash state.
@@ -2118,27 +2445,22 @@
2118
2445
  }
2119
2446
  if (state.changelogPayload) {
2120
2447
  state.availableTabs.add(VIEW_IDS.changelog)
2121
- state.availableTabs.add(VIEW_IDS.changelogSource)
2122
2448
  }
2123
2449
 
2124
2450
  if (!state.availableTabs.has(state.activeTab)) {
2125
2451
  state.activeTab = state.availableTabs.has(VIEW_IDS.leaves)
2126
2452
  ? VIEW_IDS.leaves
2127
- : state.availableTabs.has(VIEW_IDS.changelog)
2128
- ? VIEW_IDS.changelog
2129
- : VIEW_IDS.changelogSource
2453
+ : VIEW_IDS.changelog
2130
2454
  }
2131
2455
 
2132
2456
  if (state.leavesPayload && state.changelogPayload) {
2133
- statusEl.textContent =
2134
- `Loaded unified bundle with ${state.leaves.length} routes and ${
2135
- state.changelogPayload.commits?.length || 0
2136
- } commits.`
2457
+ statusEl.textContent = `Loaded unified bundle with ${state.leaves.length} routes and ${
2458
+ state.changelogPayload.commits?.length || 0
2459
+ } commits.`
2137
2460
  } else if (state.leavesPayload) {
2138
2461
  statusEl.textContent = `Loaded payload with ${state.leaves.length} routes.`
2139
2462
  } else if (state.changelogPayload) {
2140
- statusEl.textContent =
2141
- `Loaded changelog payload with ${state.changelogPayload.commits?.length || 0} commits.`
2463
+ statusEl.textContent = `Loaded changelog payload with ${state.changelogPayload.commits?.length || 0} commits.`
2142
2464
  } else {
2143
2465
  statusEl.textContent = 'No payload loaded.'
2144
2466
  }
@@ -2164,7 +2486,8 @@
2164
2486
  try {
2165
2487
  await handleFile(file)
2166
2488
  } catch (error) {
2167
- statusEl.textContent = error instanceof Error ? error.message : String(error)
2489
+ statusEl.textContent =
2490
+ error instanceof Error ? error.message : String(error)
2168
2491
  resultsEl.innerHTML = ''
2169
2492
  }
2170
2493
  })
@@ -2184,17 +2507,28 @@
2184
2507
  renderResults()
2185
2508
  })
2186
2509
 
2187
- tabLeavesBtn.addEventListener('click', () => setActiveTab(VIEW_IDS.leaves))
2188
- tabChangelogBtn.addEventListener('click', () => setActiveTab(VIEW_IDS.changelog))
2189
- tabChangelogSourceBtn.addEventListener('click', () => setActiveTab(VIEW_IDS.changelogSource))
2510
+ tabLeavesBtn.addEventListener('click', () =>
2511
+ setActiveTab(VIEW_IDS.leaves),
2512
+ )
2513
+ tabChangelogBtn.addEventListener('click', () =>
2514
+ setActiveTab(VIEW_IDS.changelog),
2515
+ )
2190
2516
 
2191
2517
  selectAllFieldsBtn.addEventListener('click', () =>
2192
2518
  setFieldSelection(new Set(currentFieldDefs().map((field) => field.id))),
2193
2519
  )
2194
- clearAllFieldsBtn.addEventListener('click', () => setFieldSelection(new Set()))
2195
- coreFieldsBtn.addEventListener('click', () => setFieldSelection(CORE_FIELD_IDS))
2196
- schemasOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(SCHEMA_SCOPE_FIELD_IDS))
2197
- sourceOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(SOURCE_FIELD_IDS))
2520
+ clearAllFieldsBtn.addEventListener('click', () =>
2521
+ setFieldSelection(new Set()),
2522
+ )
2523
+ coreFieldsBtn.addEventListener('click', () =>
2524
+ setFieldSelection(CORE_FIELD_IDS),
2525
+ )
2526
+ schemasOnlyFieldsBtn.addEventListener('click', () =>
2527
+ setFieldSelection(SCHEMA_SCOPE_FIELD_IDS),
2528
+ )
2529
+ sourceOnlyFieldsBtn.addEventListener('click', () =>
2530
+ setFieldSelection(SOURCE_FIELD_IDS),
2531
+ )
2198
2532
  resetFiltersBtn.addEventListener('click', resetFiltersToDefault)
2199
2533
 
2200
2534
  renderFieldChips()