@morscherlab/mint-sdk 1.0.39 → 1.0.42

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 (78) hide show
  1. package/dist/{ExperimentPopover-DEzCbTqo.js → ExperimentPopover-8A4Rhffp.js} +1 -1
  2. package/dist/{ExperimentPopover-mzmSfAUp.js → ExperimentPopover-BbPkIFsI.js} +8 -2
  3. package/dist/ExperimentPopover-BbPkIFsI.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-Bn0Hmg07.js → ExperimentSelectorModal-B2qek_YG.js} +91 -46
  5. package/dist/ExperimentSelectorModal-B2qek_YG.js.map +1 -0
  6. package/dist/{ExperimentSelectorModal-BAIlIybO.js → ExperimentSelectorModal-BwPbQN1g.js} +1 -1
  7. package/dist/__tests__/components/AutoGroupModal.preview.test.d.ts +1 -0
  8. package/dist/__tests__/composables/autoGroup/classKey.test.d.ts +1 -0
  9. package/dist/__tests__/composables/autoGroup/groupTree.test.d.ts +1 -0
  10. package/dist/__tests__/composables/autoGroup/tokenLength.test.d.ts +1 -0
  11. package/dist/components/AppTopBar.navigation.d.ts +0 -1
  12. package/dist/components/index.js +3 -3
  13. package/dist/{components-Cyi0IfRl.js → components-BGVwavdd.js} +5632 -5629
  14. package/dist/components-BGVwavdd.js.map +1 -0
  15. package/dist/composables/autoGroup/classKey.d.ts +1 -0
  16. package/dist/composables/autoGroup/index.d.ts +2 -1
  17. package/dist/composables/autoGroup/replicatePreGroup.d.ts +10 -12
  18. package/dist/composables/autoGroup/tokenLength.d.ts +17 -0
  19. package/dist/composables/index.d.ts +1 -1
  20. package/dist/composables/index.js +3 -3
  21. package/dist/composables/useAutoGroup.d.ts +2 -0
  22. package/dist/composables/usePluginClient.d.ts +82 -5
  23. package/dist/{composables-CFSn4NN3.js → composables-C_hPF0Gn.js} +256 -9
  24. package/dist/{composables-CFSn4NN3.js.map → composables-C_hPF0Gn.js.map} +1 -1
  25. package/dist/index.js +6 -6
  26. package/dist/install.js +3 -3
  27. package/dist/styles.css +602 -555
  28. package/dist/types/auto-group.d.ts +19 -0
  29. package/dist/{useProtocolTemplates-CXP2ZosM.js → useProtocolTemplates-BbvlHoPD.js} +218 -90
  30. package/dist/useProtocolTemplates-BbvlHoPD.js.map +1 -0
  31. package/package.json +1 -1
  32. package/src/__tests__/components/AppTopBar.navigation.test.ts +3 -5
  33. package/src/__tests__/components/AppTopBar.test.ts +2 -5
  34. package/src/__tests__/components/AppTopBarPageSelector.test.ts +22 -0
  35. package/src/__tests__/components/AutoGroupModal.preview.test.ts +46 -0
  36. package/src/__tests__/components/PluginWorkspaceView.test.ts +18 -0
  37. package/src/__tests__/composables/autoGroup/classKey.test.ts +25 -0
  38. package/src/__tests__/composables/autoGroup/fingerprint.test.ts +72 -0
  39. package/src/__tests__/composables/autoGroup/groupTree.test.ts +99 -0
  40. package/src/__tests__/composables/autoGroup/tokenLength.test.ts +85 -0
  41. package/src/__tests__/composables/useAutoGroup.test.ts +111 -19
  42. package/src/__tests__/composables/usePluginClient.test.ts +129 -3
  43. package/src/components/AppTopBar.navigation.ts +0 -2
  44. package/src/components/AppTopBar.story.vue +5 -5
  45. package/src/components/AppTopBar.vue +0 -1
  46. package/src/components/AutoGroupModal.vue +23 -19
  47. package/src/components/BaseModal.story.vue +7 -15
  48. package/src/components/ExperimentDataViewer.vue +1 -0
  49. package/src/components/ExperimentPopover.vue +6 -4
  50. package/src/components/ExperimentSelectorModal.vue +30 -3
  51. package/src/components/IconButton.story.vue +5 -0
  52. package/src/components/PluginWorkspaceView.vue +5 -1
  53. package/src/components/SampleSelector.vue +3 -2
  54. package/src/components/SampleSelectorSampleRow.vue +4 -2
  55. package/src/components/internal/AppTopBarPageSelectorInternal.vue +0 -1
  56. package/src/composables/autoGroup/classKey.ts +5 -2
  57. package/src/composables/autoGroup/columns.ts +2 -2
  58. package/src/composables/autoGroup/compose.ts +56 -0
  59. package/src/composables/autoGroup/fingerprint.ts +15 -1
  60. package/src/composables/autoGroup/index.ts +2 -0
  61. package/src/composables/autoGroup/replicatePreGroup.ts +34 -0
  62. package/src/composables/autoGroup/template.ts +2 -2
  63. package/src/composables/autoGroup/tokenLength.ts +53 -0
  64. package/src/composables/autoGroup/vocab.json +1 -2
  65. package/src/composables/index.ts +6 -0
  66. package/src/composables/useAutoGroup.ts +34 -13
  67. package/src/composables/usePluginClient.ts +453 -8
  68. package/src/styles/components/app-page-selector.css +3 -5
  69. package/src/styles/components/auto-group-modal.css +7 -11
  70. package/src/styles/components/button.css +14 -4
  71. package/src/styles/components/modal.css +3 -0
  72. package/src/styles/components/sample-selector.css +17 -0
  73. package/src/styles/variables.css +8 -0
  74. package/src/types/auto-group.ts +19 -0
  75. package/dist/ExperimentPopover-mzmSfAUp.js.map +0 -1
  76. package/dist/ExperimentSelectorModal-Bn0Hmg07.js.map +0 -1
  77. package/dist/components-Cyi0IfRl.js.map +0 -1
  78. package/dist/useProtocolTemplates-CXP2ZosM.js.map +0 -1
@@ -7,6 +7,7 @@ import type {
7
7
  SampleClass,
8
8
  } from '../../types/auto-group'
9
9
  import type { SampleGroup } from '../../types/components'
10
+ import type { TreeNode } from '../../types/componentLabTypes'
10
11
  import { DEFAULT_COLORS, QC_OVERLAY_COLOR } from './colors'
11
12
  import { classKey } from './classKey'
12
13
 
@@ -66,6 +67,14 @@ export function composeGroups(input: ComposeInput): AutoGroupResult {
66
67
  const metadata: MetadataRow[] = []
67
68
  let colorIdx = 0
68
69
 
70
+ // Nested hierarchy (class → groupBy levels → sample leaves) built alongside
71
+ // the flat groups. `treeIndex` lets repeated members reuse the same path node.
72
+ // PATH_SEP is a control char so it never collides with a real token value.
73
+ const PATH_SEP = String.fromCharCode(1)
74
+ const groupTree: TreeNode[] = []
75
+ const treeIndex = new Map<string, TreeNode>()
76
+ let treeColorIdx = 0
77
+
69
78
  for (const cls of input.classes) {
70
79
  const schema = input.schemas[classKey(cls)]
71
80
  if (!schema) continue
@@ -80,6 +89,12 @@ export function composeGroups(input: ComposeInput): AutoGroupResult {
80
89
  const groupMap = new Map<string, { name: string; samples: string[] }>()
81
90
  const colByIdx = new Map(schema.columns.map(c => [c.index, c]))
82
91
 
92
+ // Only experimental (group) classes form the nested tree; overlay/QC stay flat.
93
+ const buildsTree = cls.disposition === 'group'
94
+ const runCol = schema.columns.find(c => c.role === 'run-order')
95
+ const classNodeId = classKey(cls)
96
+ let classNode: TreeNode | null = null
97
+
83
98
  for (const m of cls.members) {
84
99
  const tokens = input.tokenizedSamples[m]
85
100
  const rowName = input.sampleNames[m]
@@ -143,6 +158,46 @@ export function composeGroups(input: ComposeInput): AutoGroupResult {
143
158
  }
144
159
 
145
160
  metadata.push({ sampleName: rowName, fields: fieldValues, group: groupKey })
161
+
162
+ // Grow the nested tree: class root → one node per groupBy column → leaf.
163
+ if (buildsTree) {
164
+ if (!classNode) {
165
+ classNode = {
166
+ id: classNodeId,
167
+ label: cls.label,
168
+ badge: 0,
169
+ children: [],
170
+ metadata: { color: DEFAULT_COLORS[treeColorIdx++ % DEFAULT_COLORS.length] },
171
+ }
172
+ treeIndex.set(classNodeId, classNode)
173
+ groupTree.push(classNode)
174
+ }
175
+ classNode.badge = (classNode.badge as number) + 1
176
+ let cursor = classNode
177
+ let pathId = classNodeId
178
+ // Reuse the already-computed groupBy values (`keyParts`) so the tree
179
+ // levels can never drift from the flat group key.
180
+ for (const val of keyParts) {
181
+ pathId += PATH_SEP + val
182
+ let node = treeIndex.get(pathId)
183
+ if (!node) {
184
+ node = { id: pathId, label: val, badge: 0, children: [] }
185
+ cursor.children!.push(node)
186
+ treeIndex.set(pathId, node)
187
+ }
188
+ node.badge = (node.badge as number) + 1
189
+ cursor = node
190
+ }
191
+ // Injection value goes through the column's op pipeline (alias/exclude)
192
+ // via `fieldValues`, matching what every other surface shows.
193
+ const injection = runCol ? fieldValues[runCol.displayName ?? runCol.name] : undefined
194
+ const leaf: TreeNode = { id: pathId + PATH_SEP + rowName, label: rowName, type: 'sample' }
195
+ if (injection) {
196
+ leaf.badge = injection
197
+ leaf.metadata = { injection }
198
+ }
199
+ cursor.children!.push(leaf)
200
+ }
146
201
  }
147
202
 
148
203
  for (const { name, samples } of groupMap.values()) {
@@ -163,5 +218,6 @@ export function composeGroups(input: ComposeInput): AutoGroupResult {
163
218
  metadata,
164
219
  excludedSamples,
165
220
  schemas: Object.values(input.schemas),
221
+ groupTree,
166
222
  }
167
223
  }
@@ -6,6 +6,7 @@ export function serializeFingerprint(schemas: ClassSchema[]): SchemaFingerprint
6
6
  classes: schemas.map(s => ({
7
7
  kind: s.classKind,
8
8
  subKind: s.subKind,
9
+ tokenLength: s.tokenLength,
9
10
  columns: s.columns.map(c => ({
10
11
  name: c.displayName ?? c.name,
11
12
  role: c.role ?? 'factor',
@@ -23,7 +24,20 @@ export function restoreFingerprint(
23
24
  current: ClassSchema[],
24
25
  ): ClassSchema[] {
25
26
  return fp.classes.map(snap => {
26
- const target = current.find(c => c.classKind === snap.kind && c.subKind === snap.subKind)
27
+ // Match on token length too when the snapshot carries one, so length-split
28
+ // classes sharing a (kind, subKind) restore to the right schema. Snapshots
29
+ // saved before token-length splitting have no `tokenLength`; for those we
30
+ // match on (kind, subKind) and prefer the candidate with the same column
31
+ // count, so an old single-class fingerprint lands on the compatible split
32
+ // instead of throwing on the first (wrong-arity) hit.
33
+ const candidates = current.filter(
34
+ c =>
35
+ c.classKind === snap.kind &&
36
+ c.subKind === snap.subKind &&
37
+ (snap.tokenLength == null || c.tokenLength === snap.tokenLength),
38
+ )
39
+ const target =
40
+ candidates.find(c => c.columns.length === snap.columns.length) ?? candidates[0]
27
41
  if (!target) {
28
42
  throw new Error(
29
43
  `Fingerprint class not present in current input: ${snap.kind}${snap.subKind ? '/' + snap.subKind : ''}`,
@@ -16,5 +16,7 @@ export {
16
16
  stripReplicateTokens,
17
17
  preGroupReplicates,
18
18
  expandGroupsWithReplicates,
19
+ expandTreeWithReplicates,
19
20
  } from './replicatePreGroup'
20
21
  export type { ReplicatePreGrouping } from './replicatePreGroup'
22
+ export { splitByTokenLength } from './tokenLength'
@@ -11,6 +11,7 @@
11
11
  * before tokenisation removes a class of fragile cardinality heuristics.
12
12
  */
13
13
 
14
+ import type { TreeNode } from '../../types/componentLabTypes'
14
15
  import vocabData from './vocab.json'
15
16
 
16
17
  const STRIP_PATTERNS: RegExp[] = (vocabData.replicateStripPatterns as string[]).map(
@@ -88,3 +89,36 @@ export function expandGroupsWithReplicates<G extends { samples: string[] }>(
88
89
  }),
89
90
  }))
90
91
  }
92
+
93
+ /**
94
+ * Expand a group hierarchy (built over base names) back to original samples.
95
+ * Each leaf node's `label` is a base name; it is replaced by one leaf per
96
+ * original sample that collapsed onto it (sharing the leaf's injection badge,
97
+ * since collapsed replicates have identical base tokens). Every ancestor's
98
+ * `badge` (sample count) is recomputed from the expanded leaf count so the tree
99
+ * agrees with the flat groups produced by `expandGroupsWithReplicates`.
100
+ */
101
+ export function expandTreeWithReplicates(
102
+ nodes: TreeNode[],
103
+ preGrouping: ReplicatePreGrouping,
104
+ ): TreeNode[] {
105
+ const baseToOriginals = new Map<string, string[]>()
106
+ for (let i = 0; i < preGrouping.baseNames.length; i++) {
107
+ baseToOriginals.set(
108
+ preGrouping.baseNames[i],
109
+ preGrouping.membersByBase[i].map(m => preGrouping.originalSamples[m]),
110
+ )
111
+ }
112
+ const leafCount = (n: TreeNode): number =>
113
+ n.children && n.children.length ? n.children.reduce((acc, c) => acc + leafCount(c), 0) : 1
114
+ const expand = (node: TreeNode): TreeNode[] => {
115
+ if (node.children && node.children.length) {
116
+ const children = node.children.flatMap(expand)
117
+ return [{ ...node, children, badge: children.reduce((acc, c) => acc + leafCount(c), 0) }]
118
+ }
119
+ const originals = baseToOriginals.get(node.label)
120
+ if (!originals || originals.length === 0) return [node]
121
+ return originals.map((name, k) => ({ ...node, id: `${node.id}${k}`, label: name }))
122
+ }
123
+ return nodes.flatMap(expand)
124
+ }
@@ -62,14 +62,14 @@ export function composeTemplate(
62
62
 
63
63
  const schemaByKey = new Map<string, ClassSchema>()
64
64
  for (const s of schemas) {
65
- const k = s.subKind ? `${s.classKind}:${s.subKind}` : s.classKind
65
+ const k = classKey({ kind: s.classKind, subKind: s.subKind, tokenLength: s.tokenLength })
66
66
  schemaByKey.set(k, s)
67
67
  }
68
68
 
69
69
  const colByNameByKey = new Map<string, Map<string, ColumnInfo>>()
70
70
  const groupBySetByKey = new Map<string, Set<number>>()
71
71
  for (const s of schemas) {
72
- const k = s.subKind ? `${s.classKind}:${s.subKind}` : s.classKind
72
+ const k = classKey({ kind: s.classKind, subKind: s.subKind, tokenLength: s.tokenLength })
73
73
  const m = new Map<string, ColumnInfo>()
74
74
  for (const c of s.columns) m.set(c.displayName ?? c.name, c)
75
75
  colByNameByKey.set(k, m)
@@ -0,0 +1,53 @@
1
+ import type { SampleClass } from '../../types/auto-group'
2
+
3
+ /**
4
+ * Token-length pre-grouping.
5
+ *
6
+ * After type classification (`detectClass`), members of one class can still have
7
+ * different token counts — e.g. a Plasma batch where some names carry an extra
8
+ * descriptor field. Splitting each class so every member shares the same token
9
+ * length is the "group same token-length sample, then extract tokens inside the
10
+ * group" step: it guarantees `buildClassSchema` sees a rectangular token matrix,
11
+ * so column index N means the same field for every sample in the group.
12
+ *
13
+ * Each output class carries `tokenLength`; `classKey` folds that into the schema
14
+ * key so length-split classes never collide. The label only gains a `· N fields`
15
+ * suffix when a (kind, subKind) genuinely spans more than one length, keeping the
16
+ * common (uniform) case visually identical to before.
17
+ */
18
+ export function splitByTokenLength(
19
+ classes: SampleClass[],
20
+ tokenized: string[][],
21
+ ): SampleClass[] {
22
+ const out: SampleClass[] = []
23
+
24
+ for (const cls of classes) {
25
+ // Bucket members by token count, preserving first-seen order of lengths.
26
+ const byLength = new Map<number, number[]>()
27
+ const lengthOrder: number[] = []
28
+ for (const m of cls.members) {
29
+ const len = tokenized[m]?.length ?? 0
30
+ const bucket = byLength.get(len)
31
+ if (bucket) {
32
+ bucket.push(m)
33
+ } else {
34
+ byLength.set(len, [m])
35
+ lengthOrder.push(len)
36
+ }
37
+ }
38
+
39
+ const multipleLengths = lengthOrder.length > 1
40
+ for (const len of lengthOrder) {
41
+ out.push({
42
+ ...cls,
43
+ members: byLength.get(len)!,
44
+ tokenLength: len,
45
+ // Drop class-tag positions that fall outside this split's token range.
46
+ classTagPositions: cls.classTagPositions.filter(p => p < len),
47
+ label: multipleLengths ? `${cls.label} · ${len} fields` : cls.label,
48
+ })
49
+ }
50
+ }
51
+
52
+ return out
53
+ }
@@ -7,7 +7,7 @@
7
7
  "matrixVocab": "Lowercase token → canonical display name. Tokens are normalised to lowercase before lookup, so adding 'tissues' handles 'Tissues' / 'TISSUES' / 'tissues' uniformly. Multiple aliases map to one canonical (e.g. cell + cells → Cells).",
8
8
  "ionizationModes": "Lowercase polarity tokens (pos / positive / neg / negative). Suffixed to the subKind so POS and NEG acquisitions get independent schemas.",
9
9
  "tissueParentTokens": "Tokens that flag the immediately-following token as the organ subKind (so `tissues / kidney` becomes Biological/Tissues, tagging both positions).",
10
- "replicateStripPatterns": "Regex sources stripped from each sample name during the replicate pre-grouping pass. Samples whose stripped (base) names match are treated as replicates of one another and grouped together before tokenisation. Default set covers: trailing run-order numbers, _T<n>, _B<n>, and _Rep<n> markers."
10
+ "replicateStripPatterns": "Regex sources stripped from each sample name during the replicate pre-grouping pass. Samples whose stripped (base) names match are treated as replicates of one another and grouped together before tokenisation. Default set covers _T<n>, _B<n>, and _Rep<n> markers. The trailing injection / run-order number is intentionally NOT stripped here — it survives tokenisation and is surfaced as a 'run-order' column (named 'Injection #'), then excluded from the default group key so replicates still collapse."
11
11
  },
12
12
 
13
13
  "patterns": {
@@ -59,7 +59,6 @@
59
59
  "tissueParentTokens": ["tissue", "tissues"],
60
60
 
61
61
  "replicateStripPatterns": [
62
- "[_-]\\d{2,4}[A-Za-z]?$",
63
62
  "[_-]T\\d+(?=[_-]|$)",
64
63
  "[_-]B\\d+(?=[_-]|$)",
65
64
  "[_-](?:rep(?:licate)?)\\d+(?=[_-]|$)"
@@ -445,6 +445,7 @@ export {
445
445
  resolvePluginBaseUrl,
446
446
  uploadPluginEndpoint,
447
447
  usePluginClient,
448
+ usePluginEventStream,
448
449
  usePluginSettings,
449
450
  useCurrentExperiment,
450
451
  type BuildPluginEndpointUrlOptions,
@@ -456,9 +457,14 @@ export {
456
457
  type PluginHttpMethod,
457
458
  type PluginNavItemContract,
458
459
  type PluginEndpointRequestOptions,
460
+ type PluginEventStreamMessage,
461
+ type PluginEventStreamOptions,
459
462
  type PluginFormDataPayload,
460
463
  type PluginFormDataValue,
461
464
  type CreatePluginClientOptions,
462
465
  type UseCurrentExperimentOptions,
463
466
  type UseCurrentExperimentReturn,
467
+ type UsePluginEventStreamReturn,
468
+ type UsePluginSettingsOptions,
469
+ type UsePluginSettingsReturn,
464
470
  } from './usePluginClient'
@@ -22,11 +22,13 @@ import {
22
22
  composeTemplate,
23
23
  detectClass,
24
24
  expandGroupsWithReplicates,
25
+ expandTreeWithReplicates,
25
26
  findMerges,
26
27
  pickPrimaryDelimiter,
27
28
  preGroupReplicates,
28
29
  restoreFingerprint,
29
30
  serializeFingerprint,
31
+ splitByTokenLength,
30
32
  splitMulti,
31
33
  type ReplicatePreGrouping,
32
34
  type TemplateOptions,
@@ -142,11 +144,13 @@ export function useAutoGroup() {
142
144
  preGrouping.value = null
143
145
  return
144
146
  }
145
- // Replicate pre-pass: strip run-order / T<n> / B<n> / Rep<n> markers, then
146
- // collapse samples whose stripped names match into one base entry. The
147
- // tokenize classify schema pipeline runs on the *base* names; the
148
- // `result` computed expands each resulting group back to the original
149
- // samples via `expandGroupsWithReplicates`.
147
+ // Replicate pre-pass: strip _T<n> / _B<n> / _Rep<n> markers, then collapse
148
+ // samples whose stripped names match into one base entry. The trailing
149
+ // injection / run-order number is deliberately NOT stripped here it
150
+ // survives tokenisation and is surfaced as a 'run-order' column below. The
151
+ // tokenize classify → split-by-length → schema pipeline runs on the
152
+ // *base* names; the `result` computed expands each resulting group back to
153
+ // the original samples via `expandGroupsWithReplicates`.
150
154
  const pre = preGroupReplicates(lines)
151
155
  preGrouping.value = pre
152
156
 
@@ -156,17 +160,27 @@ export function useAutoGroup() {
156
160
  // sampleTypeHints align to original samples, so pick the first sample's
157
161
  // hint per base group as the representative hint.
158
162
  const hints = pre.membersByBase.map(members => sampleTypeHints.value[members[0]])
159
- const detected = detectClass(tokenized.value, { sampleTypeHints: hints })
163
+ // Classify by sample type, THEN split each class by token length so every
164
+ // member of a class shares the same field count. This keeps the per-class
165
+ // token matrix rectangular, so column index N means the same field for
166
+ // every sample and the trailing injection number lands in its own column.
167
+ const detected = splitByTokenLength(
168
+ detectClass(tokenized.value, { sampleTypeHints: hints }),
169
+ tokenized.value,
170
+ )
160
171
  classes.value = detected
161
172
  const newSchemas: Record<string, ClassSchema> = {}
162
173
  for (const cls of detected) {
163
174
  const memberTokens = cls.members.map(i => tokenized.value[i])
164
- newSchemas[classKey(cls)] = buildClassSchema(
165
- memberTokens,
166
- cls.kind,
167
- cls.subKind,
168
- cls.classTagPositions,
169
- )
175
+ newSchemas[classKey(cls)] = {
176
+ ...buildClassSchema(
177
+ memberTokens,
178
+ cls.kind,
179
+ cls.subKind,
180
+ cls.classTagPositions,
181
+ ),
182
+ tokenLength: cls.tokenLength,
183
+ }
170
184
  }
171
185
  schemas.value = newSchemas
172
186
  if (detected.length > 0) {
@@ -215,6 +229,11 @@ export function useAutoGroup() {
215
229
  qcGroups: composed.qcGroups
216
230
  ? expandGroupsWithReplicates(composed.qcGroups, pre)
217
231
  : undefined,
232
+ // The tree is built over base names too; expand its leaves to the original
233
+ // samples so it agrees with the flat experimental-groups panel.
234
+ groupTree: composed.groupTree
235
+ ? expandTreeWithReplicates(composed.groupTree, pre)
236
+ : composed.groupTree,
218
237
  }
219
238
  })
220
239
 
@@ -330,7 +349,9 @@ export function useAutoGroup() {
330
349
  function loadFingerprint(fp: SchemaFingerprint) {
331
350
  const restored = restoreFingerprint(fp, Object.values(schemas.value))
332
351
  const next: Record<string, ClassSchema> = {}
333
- for (const s of restored) next[classKey({ kind: s.classKind, subKind: s.subKind })] = s
352
+ for (const s of restored) {
353
+ next[classKey({ kind: s.classKind, subKind: s.subKind, tokenLength: s.tokenLength })] = s
354
+ }
334
355
  schemas.value = next
335
356
  }
336
357