@platforma-sdk/model 1.59.3 → 1.60.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 (140) hide show
  1. package/dist/block_storage.cjs.map +1 -1
  2. package/dist/block_storage.d.ts +1 -11
  3. package/dist/block_storage.js.map +1 -1
  4. package/dist/block_storage_callbacks.cjs.map +1 -1
  5. package/dist/block_storage_callbacks.js.map +1 -1
  6. package/dist/columns/column_collection_builder.cjs +215 -0
  7. package/dist/columns/column_collection_builder.cjs.map +1 -0
  8. package/dist/columns/column_collection_builder.d.ts +112 -0
  9. package/dist/columns/column_collection_builder.js +214 -0
  10. package/dist/columns/column_collection_builder.js.map +1 -0
  11. package/dist/columns/column_selector.cjs +122 -0
  12. package/dist/columns/column_selector.cjs.map +1 -0
  13. package/dist/columns/column_selector.d.ts +41 -0
  14. package/dist/columns/column_selector.js +118 -0
  15. package/dist/columns/column_selector.js.map +1 -0
  16. package/dist/columns/column_snapshot.cjs +20 -0
  17. package/dist/columns/column_snapshot.cjs.map +1 -0
  18. package/dist/columns/column_snapshot.d.ts +39 -0
  19. package/dist/columns/column_snapshot.js +18 -0
  20. package/dist/columns/column_snapshot.js.map +1 -0
  21. package/dist/columns/column_snapshot_provider.cjs +112 -0
  22. package/dist/columns/column_snapshot_provider.cjs.map +1 -0
  23. package/dist/columns/column_snapshot_provider.d.ts +73 -0
  24. package/dist/columns/column_snapshot_provider.js +107 -0
  25. package/dist/columns/column_snapshot_provider.js.map +1 -0
  26. package/dist/columns/ctx_column_sources.cjs +84 -0
  27. package/dist/columns/ctx_column_sources.cjs.map +1 -0
  28. package/dist/columns/ctx_column_sources.d.ts +33 -0
  29. package/dist/columns/ctx_column_sources.js +82 -0
  30. package/dist/columns/ctx_column_sources.js.map +1 -0
  31. package/dist/columns/index.cjs +5 -0
  32. package/dist/columns/index.d.ts +5 -0
  33. package/dist/columns/index.js +5 -0
  34. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.cjs +111 -0
  35. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.cjs.map +1 -0
  36. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.d.ts +25 -0
  37. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.js +110 -0
  38. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.js.map +1 -0
  39. package/dist/components/PlDataTable/{table.cjs → createPlDataTable/createPlDataTableV3.cjs} +54 -54
  40. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -0
  41. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts +39 -0
  42. package/dist/components/PlDataTable/{table.js → createPlDataTable/createPlDataTableV3.js} +53 -53
  43. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -0
  44. package/dist/components/PlDataTable/createPlDataTable/index.cjs +12 -0
  45. package/dist/components/PlDataTable/createPlDataTable/index.cjs.map +1 -0
  46. package/dist/components/PlDataTable/createPlDataTable/index.d.ts +15 -0
  47. package/dist/components/PlDataTable/createPlDataTable/index.js +12 -0
  48. package/dist/components/PlDataTable/createPlDataTable/index.js.map +1 -0
  49. package/dist/components/PlDataTable/createPlDataTableSheet.cjs +18 -0
  50. package/dist/components/PlDataTable/createPlDataTableSheet.cjs.map +1 -0
  51. package/dist/components/PlDataTable/createPlDataTableSheet.d.ts +11 -0
  52. package/dist/components/PlDataTable/createPlDataTableSheet.js +17 -0
  53. package/dist/components/PlDataTable/createPlDataTableSheet.js.map +1 -0
  54. package/dist/components/PlDataTable/index.cjs +4 -1
  55. package/dist/components/PlDataTable/index.d.ts +5 -2
  56. package/dist/components/PlDataTable/index.js +4 -1
  57. package/dist/components/PlDataTable/state-migration.cjs.map +1 -1
  58. package/dist/components/PlDataTable/state-migration.d.ts +2 -2
  59. package/dist/components/PlDataTable/state-migration.js.map +1 -1
  60. package/dist/components/PlDataTable/{v4.d.ts → typesV4.d.ts} +2 -2
  61. package/dist/components/PlDataTable/{v5.d.ts → typesV5.d.ts} +2 -2
  62. package/dist/components/index.cjs +4 -1
  63. package/dist/components/index.d.ts +5 -2
  64. package/dist/components/index.js +4 -1
  65. package/dist/index.cjs +44 -16
  66. package/dist/index.d.ts +17 -5
  67. package/dist/index.js +15 -3
  68. package/dist/labels/derive_distinct_labels.cjs +156 -0
  69. package/dist/labels/derive_distinct_labels.cjs.map +1 -0
  70. package/dist/labels/derive_distinct_labels.d.ts +29 -0
  71. package/dist/labels/derive_distinct_labels.js +155 -0
  72. package/dist/labels/derive_distinct_labels.js.map +1 -0
  73. package/dist/labels/index.cjs +2 -0
  74. package/dist/labels/index.d.ts +2 -0
  75. package/dist/labels/index.js +2 -0
  76. package/dist/labels/write_labels_to_specs.cjs +15 -0
  77. package/dist/labels/write_labels_to_specs.cjs.map +1 -0
  78. package/dist/labels/write_labels_to_specs.d.ts +9 -0
  79. package/dist/labels/write_labels_to_specs.js +14 -0
  80. package/dist/labels/write_labels_to_specs.js.map +1 -0
  81. package/dist/package.cjs +1 -1
  82. package/dist/package.js +1 -1
  83. package/dist/render/api.cjs +11 -2
  84. package/dist/render/api.cjs.map +1 -1
  85. package/dist/render/api.d.ts +9 -5
  86. package/dist/render/api.js +12 -3
  87. package/dist/render/api.js.map +1 -1
  88. package/dist/render/index.d.ts +2 -1
  89. package/dist/render/index.js +1 -1
  90. package/dist/render/internal.cjs.map +1 -1
  91. package/dist/render/internal.d.ts +5 -2
  92. package/dist/render/internal.js.map +1 -1
  93. package/dist/render/util/column_collection.cjs +3 -3
  94. package/dist/render/util/column_collection.cjs.map +1 -1
  95. package/dist/render/util/column_collection.d.ts +3 -2
  96. package/dist/render/util/column_collection.js +4 -4
  97. package/dist/render/util/column_collection.js.map +1 -1
  98. package/dist/render/util/index.d.ts +2 -1
  99. package/dist/render/util/index.js +1 -1
  100. package/dist/render/util/label.cjs +7 -134
  101. package/dist/render/util/label.cjs.map +1 -1
  102. package/dist/render/util/label.d.ts +5 -50
  103. package/dist/render/util/label.js +8 -132
  104. package/dist/render/util/label.js.map +1 -1
  105. package/dist/render/util/split_selectors.d.ts +2 -2
  106. package/package.json +9 -7
  107. package/src/block_storage.ts +0 -11
  108. package/src/block_storage_callbacks.ts +1 -1
  109. package/src/columns/column_collection_builder.test.ts +427 -0
  110. package/src/columns/column_collection_builder.ts +455 -0
  111. package/src/columns/column_selector.test.ts +472 -0
  112. package/src/columns/column_selector.ts +212 -0
  113. package/src/columns/column_snapshot.ts +55 -0
  114. package/src/columns/column_snapshot_provider.ts +177 -0
  115. package/src/columns/ctx_column_sources.ts +107 -0
  116. package/src/columns/expand_by_partition.test.ts +289 -0
  117. package/src/columns/expand_by_partition.ts +187 -0
  118. package/src/columns/index.ts +5 -0
  119. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV2.ts +193 -0
  120. package/src/components/PlDataTable/{table.ts → createPlDataTable/createPlDataTableV3.ts} +134 -70
  121. package/src/components/PlDataTable/createPlDataTable/index.ts +27 -0
  122. package/src/components/PlDataTable/createPlDataTableSheet.ts +20 -0
  123. package/src/components/PlDataTable/index.ts +6 -4
  124. package/src/components/PlDataTable/state-migration.ts +2 -2
  125. package/src/index.ts +2 -1
  126. package/src/labels/derive_distinct_labels.test.ts +461 -0
  127. package/src/labels/derive_distinct_labels.ts +289 -0
  128. package/src/labels/index.ts +2 -0
  129. package/src/labels/write_labels_to_specs.ts +12 -0
  130. package/src/render/api.ts +25 -3
  131. package/src/render/internal.ts +20 -1
  132. package/src/render/util/column_collection.ts +9 -6
  133. package/src/render/util/label.test.ts +1 -1
  134. package/src/render/util/label.ts +19 -235
  135. package/src/render/util/split_selectors.ts +3 -3
  136. package/dist/components/PlDataTable/table.cjs.map +0 -1
  137. package/dist/components/PlDataTable/table.d.ts +0 -30
  138. package/dist/components/PlDataTable/table.js.map +0 -1
  139. /package/src/components/PlDataTable/{v4.ts → typesV4.ts} +0 -0
  140. /package/src/components/PlDataTable/{v5.ts → typesV5.ts} +0 -0
@@ -0,0 +1,461 @@
1
+ import { Annotation, type PColumnSpec } from "@milaboratories/pl-model-common";
2
+ import { expect, test } from "vitest";
3
+ import { deriveDistinctLabels, type Entry, type Trace } from "./derive_distinct_labels";
4
+
5
+ function tracesToSpecs(traces: Trace[]) {
6
+ return traces.map(
7
+ (t) =>
8
+ ({
9
+ kind: "PColumn",
10
+ name: "name",
11
+ valueType: "Int",
12
+ annotations: {
13
+ [Annotation.Trace]: JSON.stringify(t),
14
+ [Annotation.Label]: "Label",
15
+ },
16
+ axesSpec: [],
17
+ }) satisfies PColumnSpec,
18
+ );
19
+ }
20
+
21
+ function createSpec(overrides: Partial<PColumnSpec> = {}): PColumnSpec {
22
+ return {
23
+ kind: "PColumn",
24
+ name: "name",
25
+ valueType: "Int",
26
+ annotations: {},
27
+ axesSpec: [],
28
+ ...overrides,
29
+ } as PColumnSpec;
30
+ }
31
+ test.each<{ name: string; traces: Trace[]; labels: string[] }>([
32
+ {
33
+ name: "simple",
34
+ traces: [[{ type: "t1", label: "L1" }], [{ type: "t1", label: "L2" }]],
35
+ labels: ["L1", "L2"],
36
+ },
37
+ {
38
+ name: "later wins",
39
+ traces: [
40
+ [
41
+ { type: "t1", label: "T1L1" },
42
+ { type: "t2", label: "T2L1" },
43
+ ],
44
+ [
45
+ { type: "t1", label: "T1L2" },
46
+ { type: "t2", label: "T2L2" },
47
+ ],
48
+ ],
49
+ labels: ["T2L1", "T2L2"],
50
+ },
51
+ {
52
+ name: "importance wins",
53
+ traces: [
54
+ [
55
+ { type: "t1", importance: 100, label: "T1L1" },
56
+ { type: "t2", label: "T2L1" },
57
+ ],
58
+ [
59
+ { type: "t1", importance: 100, label: "T1L2" },
60
+ { type: "t2", label: "T2L2" },
61
+ ],
62
+ ],
63
+ labels: ["T1L1", "T1L2"],
64
+ },
65
+ {
66
+ name: "uniqueness wins",
67
+ traces: [
68
+ [
69
+ { type: "t1", label: "T1L1" },
70
+ { type: "t2", label: "T2L1" },
71
+ ],
72
+ [
73
+ { type: "t1", label: "T1L2" },
74
+ { type: "t2", label: "T2L1" },
75
+ ],
76
+ ],
77
+ labels: ["T1L1", "T1L2"],
78
+ },
79
+ {
80
+ name: "combinatoric solution",
81
+ traces: [
82
+ [
83
+ { type: "t1", label: "T1L1" },
84
+ { type: "t2", label: "T2L1" },
85
+ ],
86
+ [
87
+ { type: "t1", label: "T1L1" },
88
+ { type: "t2", label: "T2L2" },
89
+ ],
90
+ [
91
+ { type: "t1", label: "T1L2" },
92
+ { type: "t2", label: "T2L2" },
93
+ ],
94
+ ],
95
+ labels: ["T1L1 / T2L1", "T1L1 / T2L2", "T1L2 / T2L2"],
96
+ },
97
+ {
98
+ name: "different importance and id",
99
+ traces: [
100
+ [{ type: "sameType", importance: 10, id: "id1", label: "High importance" }],
101
+ [{ type: "sameType", importance: 5, id: "id2", label: "Low importance" }],
102
+ ],
103
+ labels: ["High importance", "Low importance"],
104
+ },
105
+ {
106
+ name: "mixed common and different entries",
107
+ traces: [
108
+ [
109
+ { type: "commonType", importance: 1, id: "common", label: "Common entry" },
110
+ { type: "uniqueType", importance: 10, id: "id1", label: "Unique entry 1" },
111
+ ],
112
+ [
113
+ { type: "commonType", importance: 1, id: "common", label: "Common entry" },
114
+ { type: "uniqueType", importance: 5, id: "id2", label: "Unique entry 2" },
115
+ ],
116
+ ],
117
+ labels: ["Unique entry 1", "Unique entry 2"],
118
+ },
119
+ ])("test label derivation: $name", ({ traces, labels }) => {
120
+ expect(deriveDistinctLabels(tracesToSpecs(traces)).map((r) => r.label)).toEqual(labels);
121
+ expect(
122
+ deriveDistinctLabels(tracesToSpecs(traces), { includeNativeLabel: true }).map((r) => r.label),
123
+ ).toEqual(labels.map((l) => "Label / " + l));
124
+ });
125
+
126
+ test("test fallback to native labels in label derivation", () => {
127
+ expect(deriveDistinctLabels(tracesToSpecs([[], []])).map((r) => r.label)).toEqual([
128
+ "Label",
129
+ "Label",
130
+ ]);
131
+ });
132
+
133
+ test.each<{ name: string; traces: Trace[]; labels: string[] }>([
134
+ {
135
+ name: "removes redundant low-importance type when high-importance alone suffices",
136
+ traces: [
137
+ [
138
+ { type: "t1", importance: 10, label: "High1" },
139
+ { type: "t2", importance: 1, label: "Low1" },
140
+ ],
141
+ [
142
+ { type: "t1", importance: 10, label: "High2" },
143
+ { type: "t2", importance: 1, label: "Low2" },
144
+ ],
145
+ ],
146
+ // Both t1 and t2 distinguish, but t2 (low importance) should be removed since t1 alone suffices
147
+ labels: ["High1", "High2"],
148
+ },
149
+ {
150
+ name: "keeps both types when both are needed for uniqueness",
151
+ traces: [
152
+ [
153
+ { type: "t1", importance: 10, label: "A" },
154
+ { type: "t2", importance: 1, label: "X" },
155
+ ],
156
+ [
157
+ { type: "t1", importance: 10, label: "A" },
158
+ { type: "t2", importance: 1, label: "Y" },
159
+ ],
160
+ [
161
+ { type: "t1", importance: 10, label: "B" },
162
+ { type: "t2", importance: 1, label: "Y" },
163
+ ],
164
+ ],
165
+ // Neither t1 nor t2 alone can distinguish all three, need both
166
+ labels: ["A / X", "A / Y", "B / Y"],
167
+ },
168
+ {
169
+ name: "removes multiple redundant types greedily",
170
+ traces: [
171
+ [
172
+ { type: "t1", importance: 100, label: "Unique1" },
173
+ { type: "t2", importance: 10, label: "Same" },
174
+ { type: "t3", importance: 1, label: "Same" },
175
+ ],
176
+ [
177
+ { type: "t1", importance: 100, label: "Unique2" },
178
+ { type: "t2", importance: 10, label: "Same" },
179
+ { type: "t3", importance: 1, label: "Same" },
180
+ ],
181
+ ],
182
+ // t1 alone distinguishes; t2 and t3 are redundant and should be removed
183
+ labels: ["Unique1", "Unique2"],
184
+ },
185
+ {
186
+ name: "fallback case: removes types that do not reduce cardinality",
187
+ traces: [
188
+ // Two columns with identical traces - cannot be distinguished
189
+ [
190
+ { type: "t1", importance: 100, label: "A" },
191
+ { type: "t2", importance: 10, label: "X" },
192
+ { type: "t3", importance: 1, label: "Same" },
193
+ ],
194
+ [
195
+ { type: "t1", importance: 100, label: "A" },
196
+ { type: "t2", importance: 10, label: "X" },
197
+ { type: "t3", importance: 1, label: "Same" },
198
+ ],
199
+ // Third column is different
200
+ [
201
+ { type: "t1", importance: 100, label: "B" },
202
+ { type: "t2", importance: 10, label: "Y" },
203
+ { type: "t3", importance: 1, label: "Same" },
204
+ ],
205
+ ],
206
+ // Cannot achieve full uniqueness (2 columns are identical), but t3 (Same) can be removed
207
+ // since it doesn't help distinguish anything. t1 alone gives cardinality 2.
208
+ labels: ["A", "A", "B"],
209
+ },
210
+ ])("test label minimization: $name", ({ traces, labels }) => {
211
+ expect(deriveDistinctLabels(tracesToSpecs(traces)).map((r) => r.label)).toEqual(labels);
212
+ });
213
+
214
+ test.each<{ name: string; traces: Trace[]; labels: string[]; forceTraceElements: string[] }>([
215
+ {
216
+ name: "force one element",
217
+ traces: [
218
+ [
219
+ { type: "t1", label: "T1L1" },
220
+ { type: "t2", label: "T2L1" },
221
+ ],
222
+ [
223
+ { type: "t1", label: "T1L2" },
224
+ { type: "t2", label: "T2L2" },
225
+ ],
226
+ ],
227
+ labels: ["T1L1", "T1L2"],
228
+ forceTraceElements: ["t1"],
229
+ },
230
+ {
231
+ name: "force multiple elements",
232
+ traces: [
233
+ [
234
+ { type: "t1", label: "T1L1" },
235
+ { type: "t2", label: "T2L1" },
236
+ { type: "t3", label: "T3L1" },
237
+ ],
238
+ [
239
+ { type: "t1", label: "T1L2" },
240
+ { type: "t2", label: "T2L2" },
241
+ { type: "t3", label: "T3L2" },
242
+ ],
243
+ ],
244
+ labels: ["T1L1 / T3L1", "T1L2 / T3L2"],
245
+ forceTraceElements: ["t1", "t3"],
246
+ },
247
+ {
248
+ name: "force element not in all traces",
249
+ traces: [
250
+ [
251
+ { type: "t1", label: "T1L1" },
252
+ { type: "t2", label: "T2L1" },
253
+ ],
254
+ [{ type: "t2", label: "T2L2" }],
255
+ ],
256
+ labels: ["T1L1 / T2L1", "T2L2"],
257
+ forceTraceElements: ["t1"],
258
+ },
259
+ {
260
+ name: "force element with includeNativeLabel",
261
+ traces: [
262
+ [
263
+ { type: "t1", label: "T1L1" },
264
+ { type: "t2", label: "T2L1" },
265
+ ],
266
+ [
267
+ { type: "t1", label: "T1L2" },
268
+ { type: "t2", label: "T2L2" },
269
+ ],
270
+ ],
271
+ labels: ["T1L1", "T1L2"],
272
+ forceTraceElements: ["t1"],
273
+ },
274
+ ])(
275
+ "test label derivation with forceTraceElements: $name",
276
+ ({ name, traces, labels, forceTraceElements }) => {
277
+ expect(
278
+ deriveDistinctLabels(tracesToSpecs(traces), { forceTraceElements }).map((r) => r.label),
279
+ ).toEqual(labels);
280
+
281
+ if (name === "force element with includeNativeLabel") {
282
+ expect(
283
+ deriveDistinctLabels(tracesToSpecs(traces), {
284
+ forceTraceElements,
285
+ includeNativeLabel: true,
286
+ }).map((r) => r.label),
287
+ ).toEqual(labels.map((l) => "Label / " + l));
288
+ }
289
+ },
290
+ );
291
+
292
+ // --- Entry with { spec, prefixTrace, suffixTrace } ---
293
+
294
+ test("Entry with prefixTrace prepends to labels", () => {
295
+ const spec = createSpec({
296
+ annotations: {
297
+ [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Base" }]),
298
+ },
299
+ });
300
+ const entries: Entry[] = [
301
+ { spec, prefixTrace: [{ type: "prefix", label: "P1" }] },
302
+ { spec, prefixTrace: [{ type: "prefix", label: "P2" }] },
303
+ ];
304
+ const labels = deriveDistinctLabels(entries).map((r) => r.label);
305
+ expect(labels).toEqual(["P1", "P2"]);
306
+ });
307
+
308
+ test("Entry with suffixTrace appends to labels", () => {
309
+ const spec = createSpec({
310
+ annotations: {
311
+ [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Base" }]),
312
+ },
313
+ });
314
+ const entries: Entry[] = [
315
+ { spec, suffixTrace: [{ type: "suffix", label: "S1" }] },
316
+ { spec, suffixTrace: [{ type: "suffix", label: "S2" }] },
317
+ ];
318
+ const labels = deriveDistinctLabels(entries).map((r) => r.label);
319
+ expect(labels).toEqual(["S1", "S2"]);
320
+ });
321
+
322
+ test("Entry with both prefixTrace and suffixTrace", () => {
323
+ const spec1 = createSpec({
324
+ annotations: {
325
+ [Annotation.Trace]: JSON.stringify([{ type: "base", label: "Same" }]),
326
+ },
327
+ });
328
+ const entries: Entry[] = [
329
+ {
330
+ spec: spec1,
331
+ prefixTrace: [{ type: "pfx", label: "Pre1" }],
332
+ suffixTrace: [{ type: "sfx", label: "Suf1" }],
333
+ },
334
+ {
335
+ spec: spec1,
336
+ prefixTrace: [{ type: "pfx", label: "Pre2" }],
337
+ suffixTrace: [{ type: "sfx", label: "Suf2" }],
338
+ },
339
+ ];
340
+ const labels = deriveDistinctLabels(entries).map((r) => r.label);
341
+ // suffix is later in the trace (higher positional importance), so it wins over prefix
342
+ expect(labels).toEqual(["Suf1", "Suf2"]);
343
+ });
344
+
345
+ // --- addLabelAsSuffix ---
346
+
347
+ test("addLabelAsSuffix places native label at the end", () => {
348
+ const specs = tracesToSpecs([[{ type: "t1", label: "L1" }], [{ type: "t1", label: "L2" }]]);
349
+ const labels = deriveDistinctLabels(specs, {
350
+ includeNativeLabel: true,
351
+ addLabelAsSuffix: true,
352
+ }).map((r) => r.label);
353
+ expect(labels).toEqual(["L1 / Label", "L2 / Label"]);
354
+ });
355
+
356
+ // --- separator ---
357
+
358
+ test("custom separator is used between label parts", () => {
359
+ const specs = tracesToSpecs([
360
+ [
361
+ { type: "t1", label: "A" },
362
+ { type: "t2", label: "X" },
363
+ ],
364
+ [
365
+ { type: "t1", label: "A" },
366
+ { type: "t2", label: "Y" },
367
+ ],
368
+ [
369
+ { type: "t1", label: "B" },
370
+ { type: "t2", label: "Y" },
371
+ ],
372
+ ]);
373
+ const labels = deriveDistinctLabels(specs, { separator: " - " }).map((r) => r.label);
374
+ expect(labels).toEqual(["A - X", "A - Y", "B - Y"]);
375
+ });
376
+
377
+ // --- single value ---
378
+
379
+ test("single value gets its trace label", () => {
380
+ const specs = tracesToSpecs([[{ type: "t1", label: "Only" }]]);
381
+ const labels = deriveDistinctLabels(specs).map((r) => r.label);
382
+ expect(labels).toEqual(["Only"]);
383
+ });
384
+
385
+ // --- Unlabeled fallback ---
386
+
387
+ test("Unlabeled fallback when no trace entries match", () => {
388
+ // Two identical specs with identical traces — fallback path
389
+ const spec = createSpec({
390
+ annotations: {
391
+ [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Same" }]),
392
+ },
393
+ });
394
+ // Remove native label so LABEL_TYPE is not added
395
+ delete spec.annotations![Annotation.Label];
396
+
397
+ const result = deriveDistinctLabels([spec, spec]);
398
+ expect(result.every((r) => r.label === "Same")).toBe(true);
399
+ });
400
+
401
+ test("Unlabeled when no traces and no label", () => {
402
+ const spec = createSpec();
403
+ const result = deriveDistinctLabels([spec, spec]);
404
+ expect(result.every((r) => r.label === "Unlabeled")).toBe(true);
405
+ });
406
+
407
+ // --- repeated type occurrences (secondaryTypes path) ---
408
+
409
+ test("repeated type occurrences are used as secondary types", () => {
410
+ // Two records where "t1" appears twice in each, with different labels on 2nd occurrence
411
+ const specs = tracesToSpecs([
412
+ [
413
+ { type: "t1", label: "First" },
414
+ { type: "t1", label: "A" },
415
+ ],
416
+ [
417
+ { type: "t1", label: "First" },
418
+ { type: "t1", label: "B" },
419
+ ],
420
+ ]);
421
+ const labels = deriveDistinctLabels(specs).map((r) => r.label);
422
+ // t1@1 has label "First" for both (same), t1@2 has "A" vs "B" (distinguishing)
423
+ // t1@2 is secondary since it only appears when there are 2 occurrences
424
+ expect(labels).toEqual(["A", "B"]);
425
+ });
426
+
427
+ // --- spec without Annotation.Label (only Trace) ---
428
+
429
+ test("spec without native label uses only trace entries", () => {
430
+ const specs = [
431
+ createSpec({
432
+ annotations: {
433
+ [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "X" }]),
434
+ },
435
+ }),
436
+ createSpec({
437
+ annotations: {
438
+ [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Y" }]),
439
+ },
440
+ }),
441
+ ];
442
+ const labels = deriveDistinctLabels(specs).map((r) => r.label);
443
+ expect(labels).toEqual(["X", "Y"]);
444
+ });
445
+
446
+ test("includeNativeLabel with no native label does not break", () => {
447
+ const specs = [
448
+ createSpec({
449
+ annotations: {
450
+ [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "X" }]),
451
+ },
452
+ }),
453
+ createSpec({
454
+ annotations: {
455
+ [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Y" }]),
456
+ },
457
+ }),
458
+ ];
459
+ const labels = deriveDistinctLabels(specs, { includeNativeLabel: true }).map((r) => r.label);
460
+ expect(labels).toEqual(["X", "Y"]);
461
+ });