@platforma-sdk/model 1.63.12 → 1.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/columns/column_collection_builder.cjs +105 -92
  2. package/dist/columns/column_collection_builder.cjs.map +1 -1
  3. package/dist/columns/column_collection_builder.d.ts +13 -12
  4. package/dist/columns/column_collection_builder.d.ts.map +1 -1
  5. package/dist/columns/column_collection_builder.js +107 -94
  6. package/dist/columns/column_collection_builder.js.map +1 -1
  7. package/dist/columns/column_selector.cjs +8 -80
  8. package/dist/columns/column_selector.cjs.map +1 -1
  9. package/dist/columns/column_selector.d.ts +6 -14
  10. package/dist/columns/column_selector.d.ts.map +1 -1
  11. package/dist/columns/column_selector.js +6 -77
  12. package/dist/columns/column_selector.js.map +1 -1
  13. package/dist/columns/column_snapshot.cjs +3 -3
  14. package/dist/columns/column_snapshot.cjs.map +1 -1
  15. package/dist/columns/column_snapshot.d.ts +3 -3
  16. package/dist/columns/column_snapshot.d.ts.map +1 -1
  17. package/dist/columns/column_snapshot.js +3 -3
  18. package/dist/columns/column_snapshot.js.map +1 -1
  19. package/dist/columns/column_snapshot_provider.cjs +1 -1
  20. package/dist/columns/column_snapshot_provider.cjs.map +1 -1
  21. package/dist/columns/column_snapshot_provider.d.ts +8 -8
  22. package/dist/columns/column_snapshot_provider.d.ts.map +1 -1
  23. package/dist/columns/column_snapshot_provider.js +1 -1
  24. package/dist/columns/column_snapshot_provider.js.map +1 -1
  25. package/dist/columns/ctx_column_sources.cjs.map +1 -1
  26. package/dist/columns/ctx_column_sources.d.ts +2 -1
  27. package/dist/columns/ctx_column_sources.d.ts.map +1 -1
  28. package/dist/columns/ctx_column_sources.js.map +1 -1
  29. package/dist/columns/expand_by_partition.cjs +106 -0
  30. package/dist/columns/expand_by_partition.cjs.map +1 -0
  31. package/dist/columns/expand_by_partition.d.ts +33 -0
  32. package/dist/columns/expand_by_partition.d.ts.map +1 -0
  33. package/dist/columns/expand_by_partition.js +105 -0
  34. package/dist/columns/expand_by_partition.js.map +1 -0
  35. package/dist/columns/index.cjs +1 -0
  36. package/dist/columns/index.d.ts +4 -3
  37. package/dist/columns/index.js +1 -0
  38. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs +26 -0
  39. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs.map +1 -0
  40. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js +25 -0
  41. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js.map +1 -0
  42. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.cjs +68 -0
  43. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.cjs.map +1 -0
  44. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.js +67 -0
  45. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.js.map +1 -0
  46. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.cjs +27 -17
  47. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.cjs.map +1 -1
  48. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.d.ts +4 -0
  49. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.d.ts.map +1 -1
  50. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.js +28 -18
  51. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.js.map +1 -1
  52. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs +258 -175
  53. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -1
  54. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts +37 -21
  55. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts.map +1 -1
  56. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js +261 -175
  57. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -1
  58. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs +64 -0
  59. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs.map +1 -0
  60. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts +17 -0
  61. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts.map +1 -0
  62. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js +63 -0
  63. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js.map +1 -0
  64. package/dist/components/PlDataTable/createPlDataTable/index.cjs +2 -1
  65. package/dist/components/PlDataTable/createPlDataTable/index.cjs.map +1 -1
  66. package/dist/components/PlDataTable/createPlDataTable/index.d.ts +2 -1
  67. package/dist/components/PlDataTable/createPlDataTable/index.d.ts.map +1 -1
  68. package/dist/components/PlDataTable/createPlDataTable/index.js +2 -1
  69. package/dist/components/PlDataTable/createPlDataTable/index.js.map +1 -1
  70. package/dist/components/PlDataTable/createPlDataTable/utils.cjs +109 -0
  71. package/dist/components/PlDataTable/createPlDataTable/utils.cjs.map +1 -0
  72. package/dist/components/PlDataTable/createPlDataTable/utils.d.ts +19 -0
  73. package/dist/components/PlDataTable/createPlDataTable/utils.d.ts.map +1 -0
  74. package/dist/components/PlDataTable/createPlDataTable/utils.js +102 -0
  75. package/dist/components/PlDataTable/createPlDataTable/utils.js.map +1 -0
  76. package/dist/components/PlDataTable/index.cjs +3 -1
  77. package/dist/components/PlDataTable/index.d.ts +5 -3
  78. package/dist/components/PlDataTable/index.js +3 -1
  79. package/dist/components/PlDataTable/labels.cjs +25 -11
  80. package/dist/components/PlDataTable/labels.cjs.map +1 -1
  81. package/dist/components/PlDataTable/labels.js +25 -11
  82. package/dist/components/PlDataTable/labels.js.map +1 -1
  83. package/dist/components/PlDataTable/state-migration.cjs +8 -2
  84. package/dist/components/PlDataTable/state-migration.cjs.map +1 -1
  85. package/dist/components/PlDataTable/state-migration.d.ts.map +1 -1
  86. package/dist/components/PlDataTable/state-migration.js +8 -2
  87. package/dist/components/PlDataTable/state-migration.js.map +1 -1
  88. package/dist/components/PlDataTable/typesV5.d.ts +23 -15
  89. package/dist/components/PlDataTable/typesV5.d.ts.map +1 -1
  90. package/dist/components/index.cjs +3 -1
  91. package/dist/components/index.d.ts +4 -2
  92. package/dist/components/index.js +3 -1
  93. package/dist/index.cjs +13 -9
  94. package/dist/index.d.ts +9 -7
  95. package/dist/index.js +6 -4
  96. package/dist/labels/derive_distinct_labels.cjs +39 -27
  97. package/dist/labels/derive_distinct_labels.cjs.map +1 -1
  98. package/dist/labels/derive_distinct_labels.d.ts +15 -15
  99. package/dist/labels/derive_distinct_labels.d.ts.map +1 -1
  100. package/dist/labels/derive_distinct_labels.js +39 -27
  101. package/dist/labels/derive_distinct_labels.js.map +1 -1
  102. package/dist/labels/index.cjs +0 -1
  103. package/dist/labels/index.d.ts +1 -2
  104. package/dist/labels/index.js +0 -1
  105. package/dist/package.cjs +1 -1
  106. package/dist/package.js +1 -1
  107. package/dist/render/api.cjs +10 -3
  108. package/dist/render/api.cjs.map +1 -1
  109. package/dist/render/api.d.ts +2 -2
  110. package/dist/render/api.d.ts.map +1 -1
  111. package/dist/render/api.js +10 -3
  112. package/dist/render/api.js.map +1 -1
  113. package/dist/render/util/column_collection.cjs +3 -3
  114. package/dist/render/util/column_collection.cjs.map +1 -1
  115. package/dist/render/util/column_collection.d.ts.map +1 -1
  116. package/dist/render/util/column_collection.js +3 -3
  117. package/dist/render/util/column_collection.js.map +1 -1
  118. package/dist/render/util/label.cjs +2 -2
  119. package/dist/render/util/label.cjs.map +1 -1
  120. package/dist/render/util/label.js +2 -2
  121. package/dist/render/util/label.js.map +1 -1
  122. package/dist/render/util/pcolumn_data.cjs.map +1 -1
  123. package/dist/render/util/pcolumn_data.d.ts +2 -2
  124. package/dist/render/util/pcolumn_data.d.ts.map +1 -1
  125. package/dist/render/util/pcolumn_data.js.map +1 -1
  126. package/package.json +7 -7
  127. package/src/columns/column_collection_builder.test.ts +40 -27
  128. package/src/columns/column_collection_builder.ts +176 -131
  129. package/src/columns/column_selector.test.ts +17 -399
  130. package/src/columns/column_selector.ts +14 -127
  131. package/src/columns/column_snapshot.ts +5 -5
  132. package/src/columns/column_snapshot_provider.ts +11 -10
  133. package/src/columns/ctx_column_sources.ts +2 -2
  134. package/src/columns/expand_by_partition.test.ts +4 -4
  135. package/src/columns/expand_by_partition.ts +4 -3
  136. package/src/columns/index.ts +1 -0
  137. package/src/components/PlDataTable/createPlDataTable/createPTableDefV2.ts +42 -0
  138. package/src/components/PlDataTable/createPlDataTable/createPTableDefV3.ts +89 -0
  139. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV2.ts +51 -19
  140. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts +500 -313
  141. package/src/components/PlDataTable/createPlDataTable/discoverColumns.ts +122 -0
  142. package/src/components/PlDataTable/createPlDataTable/index.ts +4 -2
  143. package/src/components/PlDataTable/createPlDataTable/utils.test.ts +257 -0
  144. package/src/components/PlDataTable/createPlDataTable/utils.ts +160 -0
  145. package/src/components/PlDataTable/index.ts +15 -2
  146. package/src/components/PlDataTable/labels.ts +29 -18
  147. package/src/components/PlDataTable/state-migration.ts +6 -1
  148. package/src/components/PlDataTable/typesV5.ts +25 -12
  149. package/src/labels/derive_distinct_labels.test.ts +143 -45
  150. package/src/labels/derive_distinct_labels.ts +102 -49
  151. package/src/labels/index.ts +0 -1
  152. package/src/render/api.ts +15 -5
  153. package/src/render/util/column_collection.ts +4 -3
  154. package/src/render/util/label.ts +2 -2
  155. package/src/render/util/pcolumn_data.ts +5 -3
  156. package/dist/labels/write_labels_to_specs.cjs +0 -14
  157. package/dist/labels/write_labels_to_specs.cjs.map +0 -1
  158. package/dist/labels/write_labels_to_specs.d.ts +0 -7
  159. package/dist/labels/write_labels_to_specs.d.ts.map +0 -1
  160. package/dist/labels/write_labels_to_specs.js +0 -13
  161. package/dist/labels/write_labels_to_specs.js.map +0 -1
  162. package/src/labels/write_labels_to_specs.ts +0 -12
@@ -3,7 +3,6 @@ import type {
3
3
  AxisSpec,
4
4
  CanonicalizedJson,
5
5
  ListOptionBase,
6
- PObjectId,
7
6
  PTableColumnSpec,
8
7
  PTableSorting,
9
8
  PColumnIdAndSpec,
@@ -13,6 +12,7 @@ import type {
13
12
  PFrameHandle,
14
13
  } from "@milaboratories/pl-model-common";
15
14
  import type { FilterSpecLeaf } from "../../filters";
15
+ import { Nil } from "@milaboratories/helpers";
16
16
 
17
17
  export type PlTableColumnId = {
18
18
  /** Original column spec */
@@ -63,10 +63,17 @@ export type PlDataTableSheetState = {
63
63
  };
64
64
 
65
65
  /** Tree-based filter state compatible with PlAdvancedFilter's RootFilter */
66
- export type PlDataTableFilters = RootFilterSpec<FilterSpecLeaf<CanonicalizedJson<PTableColumnId>>>;
66
+ export type PlDataTableFilterMeta = {
67
+ id: number;
68
+ source?: "table-filter" | "table-search";
69
+ isExpanded?: boolean;
70
+ isSuppressed?: boolean;
71
+ };
72
+ export type PlDataTableFilterSpecLeaf = FilterSpecLeaf<CanonicalizedJson<PTableColumnId>>;
73
+ export type PlDataTableFilters = RootFilterSpec<PlDataTableFilterSpecLeaf>;
67
74
  export type PlDataTableFiltersWithMeta = RootFilterSpec<
68
- FilterSpecLeaf<CanonicalizedJson<PTableColumnId>>,
69
- { id: number; isExpanded?: boolean; source?: "table-filter" | "table-search" }
75
+ PlDataTableFilterSpecLeaf,
76
+ PlDataTableFilterMeta
70
77
  >;
71
78
 
72
79
  export type PlDataTableStateV2CacheEntry = {
@@ -76,8 +83,10 @@ export type PlDataTableStateV2CacheEntry = {
76
83
  gridState: PlDataTableGridStateCore;
77
84
  /** Sheets state */
78
85
  sheetsState: PlDataTableSheetState[];
79
- /** Filters state (tree-based, compatible with PlAdvancedFilter) */
86
+ /** User filters state (tree-based, compatible with PlAdvancedFilter) */
80
87
  filtersState: null | PlDataTableFiltersWithMeta;
88
+ /** Default filters state from model (snapshot of defaults) */
89
+ defaultFiltersState: null | PlDataTableFiltersWithMeta;
81
90
  /** Fast search string */
82
91
  searchString?: string;
83
92
  };
@@ -86,14 +95,16 @@ export type PTableParamsV2 =
86
95
  | {
87
96
  sourceId: null;
88
97
  hiddenColIds: null;
89
- filters: null;
90
98
  sorting: [];
99
+ filters: null;
100
+ defaultFilters: null;
91
101
  }
92
102
  | {
93
103
  sourceId: string;
94
- hiddenColIds: null | PObjectId[];
95
- filters: null | PlDataTableFilters;
104
+ hiddenColIds: null | PTableColumnId[];
96
105
  sorting: PTableSorting[];
106
+ filters: null | PlDataTableFilters;
107
+ defaultFilters: null | PlDataTableFilters;
97
108
  };
98
109
 
99
110
  export type PlDataTableStateV2Normalized = {
@@ -108,13 +119,15 @@ export type PlDataTableStateV2Normalized = {
108
119
  /** PlAgDataTable model */
109
120
  export type PlDataTableModel = {
110
121
  /** DataSource identifier for state management */
111
- sourceId: string | null;
122
+ sourceId: null | string;
112
123
  /** p-table including all columns, used to show the full specification of the table */
113
- fullTableHandle: PTableHandle;
124
+ fullTableHandle?: PTableHandle;
114
125
  /** p-frame handle */
115
- fullPframeHandle: PFrameHandle;
126
+ fullPframeHandle?: PFrameHandle;
116
127
  /** p-table including only visible columns, used to get the data */
117
- visibleTableHandle: PTableHandle;
128
+ visibleTableHandle?: PTableHandle;
129
+ /** Default filters from model options, surfaced for UI display */
130
+ defaultFilters?: Nil | PlDataTableFilters;
118
131
  };
119
132
 
120
133
  export type CreatePlDataTableOps = {
@@ -117,17 +117,14 @@ test.each<{ name: string; traces: Trace[]; labels: string[] }>([
117
117
  labels: ["Unique entry 1", "Unique entry 2"],
118
118
  },
119
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));
120
+ expect(deriveDistinctLabels(tracesToSpecs(traces))).toEqual(labels);
121
+ expect(deriveDistinctLabels(tracesToSpecs(traces), { includeNativeLabel: true })).toEqual(
122
+ labels.map((l) => "Label / " + l),
123
+ );
124
124
  });
125
125
 
126
126
  test("test fallback to native labels in label derivation", () => {
127
- expect(deriveDistinctLabels(tracesToSpecs([[], []])).map((r) => r.label)).toEqual([
128
- "Label",
129
- "Label",
130
- ]);
127
+ expect(deriveDistinctLabels(tracesToSpecs([[], []]))).toEqual(["Label", "Label"]);
131
128
  });
132
129
 
133
130
  test.each<{ name: string; traces: Trace[]; labels: string[] }>([
@@ -208,7 +205,7 @@ test.each<{ name: string; traces: Trace[]; labels: string[] }>([
208
205
  labels: ["A", "A", "B"],
209
206
  },
210
207
  ])("test label minimization: $name", ({ traces, labels }) => {
211
- expect(deriveDistinctLabels(tracesToSpecs(traces)).map((r) => r.label)).toEqual(labels);
208
+ expect(deriveDistinctLabels(tracesToSpecs(traces))).toEqual(labels);
212
209
  });
213
210
 
214
211
  test.each<{ name: string; traces: Trace[]; labels: string[]; forceTraceElements: string[] }>([
@@ -274,72 +271,173 @@ test.each<{ name: string; traces: Trace[]; labels: string[]; forceTraceElements:
274
271
  ])(
275
272
  "test label derivation with forceTraceElements: $name",
276
273
  ({ name, traces, labels, forceTraceElements }) => {
277
- expect(
278
- deriveDistinctLabels(tracesToSpecs(traces), { forceTraceElements }).map((r) => r.label),
279
- ).toEqual(labels);
274
+ expect(deriveDistinctLabels(tracesToSpecs(traces), { forceTraceElements })).toEqual(labels);
280
275
 
281
276
  if (name === "force element with includeNativeLabel") {
282
277
  expect(
283
278
  deriveDistinctLabels(tracesToSpecs(traces), {
284
279
  forceTraceElements,
285
280
  includeNativeLabel: true,
286
- }).map((r) => r.label),
281
+ }),
287
282
  ).toEqual(labels.map((l) => "Label / " + l));
288
283
  }
289
284
  },
290
285
  );
291
286
 
292
- // --- Entry with { spec, prefixTrace, suffixTrace } ---
287
+ // --- Entry with { spec, extraTrace } ---
293
288
 
294
- test("Entry with prefixTrace prepends to labels", () => {
289
+ test("Entry with extraTrace (suffix, default) appends to labels", () => {
295
290
  const spec = createSpec({
296
291
  annotations: {
297
292
  [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Base" }]),
298
293
  },
299
294
  });
300
295
  const entries: Entry[] = [
301
- { spec, prefixTrace: [{ type: "prefix", label: "P1" }] },
302
- { spec, prefixTrace: [{ type: "prefix", label: "P2" }] },
296
+ { spec, extraTrace: [{ type: "suffix", label: "S1" }] },
297
+ { spec, extraTrace: [{ type: "suffix", label: "S2" }] },
303
298
  ];
304
- const labels = deriveDistinctLabels(entries).map((r) => r.label);
305
- expect(labels).toEqual(["P1", "P2"]);
299
+ const labels = deriveDistinctLabels(entries);
300
+ expect(labels).toEqual(["S1", "S2"]);
306
301
  });
307
302
 
308
- test("Entry with suffixTrace appends to labels", () => {
303
+ test("Entry with extraTrace position prefix prepends to labels", () => {
309
304
  const spec = createSpec({
310
305
  annotations: {
311
306
  [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Base" }]),
312
307
  },
313
308
  });
314
309
  const entries: Entry[] = [
315
- { spec, suffixTrace: [{ type: "suffix", label: "S1" }] },
316
- { spec, suffixTrace: [{ type: "suffix", label: "S2" }] },
310
+ { spec, extraTrace: [{ type: "prefix", label: "P1", position: "prefix" }] },
311
+ { spec, extraTrace: [{ type: "prefix", label: "P2", position: "prefix" }] },
317
312
  ];
318
- const labels = deriveDistinctLabels(entries).map((r) => r.label);
319
- expect(labels).toEqual(["S1", "S2"]);
313
+ const labels = deriveDistinctLabels(entries);
314
+ expect(labels).toEqual(["P1", "P2"]);
320
315
  });
321
316
 
322
- test("Entry with both prefixTrace and suffixTrace", () => {
323
- const spec1 = createSpec({
324
- annotations: {
325
- [Annotation.Trace]: JSON.stringify([{ type: "base", label: "Same" }]),
317
+ // --- linkerPath ---
318
+
319
+ test("linkerPath appends default 'via' suffix", () => {
320
+ const entries: Entry[] = [
321
+ {
322
+ spec: createSpec({
323
+ annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col1" }]) },
324
+ }),
325
+ linkerPath: [
326
+ {
327
+ spec: createSpec({
328
+ annotations: { [Annotation.LinkLabel]: "MyLinker" },
329
+ }),
330
+ },
331
+ ],
332
+ },
333
+ {
334
+ spec: createSpec({
335
+ annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col2" }]) },
336
+ }),
326
337
  },
338
+ ];
339
+ const labels = deriveDistinctLabels(entries);
340
+ expect(labels).toEqual(["Col1 via MyLinker", "Col2"]);
341
+ });
342
+
343
+ test("linkerPath with multiple steps joins with ' > '", () => {
344
+ const entries: Entry[] = [
345
+ {
346
+ spec: createSpec({
347
+ annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col1" }]) },
348
+ }),
349
+ linkerPath: [
350
+ { spec: createSpec({ annotations: { [Annotation.LinkLabel]: "L1" } }) },
351
+ { spec: createSpec({ annotations: { [Annotation.LinkLabel]: "L2" } }) },
352
+ ],
353
+ },
354
+ {
355
+ spec: createSpec({
356
+ annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col2" }]) },
357
+ }),
358
+ },
359
+ ];
360
+ const labels = deriveDistinctLabels(entries);
361
+ expect(labels).toEqual(["Col1 via L1 > L2", "Col2"]);
362
+ });
363
+
364
+ test("linkerPath skips steps without labels", () => {
365
+ const entries: Entry[] = [
366
+ {
367
+ spec: createSpec({
368
+ annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col1" }]) },
369
+ }),
370
+ linkerPath: [
371
+ { spec: createSpec() },
372
+ { spec: createSpec({ annotations: { [Annotation.LinkLabel]: "L2" } }) },
373
+ ],
374
+ },
375
+ {
376
+ spec: createSpec({
377
+ annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col2" }]) },
378
+ }),
379
+ },
380
+ ];
381
+ const labels = deriveDistinctLabels(entries);
382
+ expect(labels).toEqual(["Col1 via L2", "Col2"]);
383
+ });
384
+
385
+ test("linkerPath with custom linkerLabelFormatter", () => {
386
+ const entries: Entry[] = [
387
+ {
388
+ spec: createSpec({
389
+ annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col1" }]) },
390
+ }),
391
+ linkerPath: [{ spec: createSpec({ annotations: { [Annotation.LinkLabel]: "L1" } }) }],
392
+ },
393
+ {
394
+ spec: createSpec({
395
+ annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col2" }]) },
396
+ }),
397
+ },
398
+ ];
399
+ const labels = deriveDistinctLabels(entries, {
400
+ linkerLabelFormatter: (linkerLabels) => `[${linkerLabels.join(", ")}]`,
327
401
  });
402
+ expect(labels).toEqual(["Col1 [L1]", "Col2"]);
403
+ });
404
+
405
+ test("linkerPath with linkerLabelFormatter returning undefined suppresses suffix", () => {
328
406
  const entries: Entry[] = [
329
407
  {
330
- spec: spec1,
331
- prefixTrace: [{ type: "pfx", label: "Pre1" }],
332
- suffixTrace: [{ type: "sfx", label: "Suf1" }],
408
+ spec: createSpec({
409
+ annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col1" }]) },
410
+ }),
411
+ linkerPath: [{ spec: createSpec({ annotations: { [Annotation.LinkLabel]: "L1" } }) }],
333
412
  },
334
413
  {
335
- spec: spec1,
336
- prefixTrace: [{ type: "pfx", label: "Pre2" }],
337
- suffixTrace: [{ type: "sfx", label: "Suf2" }],
414
+ spec: createSpec({
415
+ annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col2" }]) },
416
+ }),
338
417
  },
339
418
  ];
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"]);
419
+ const labels = deriveDistinctLabels(entries, {
420
+ linkerLabelFormatter: () => undefined,
421
+ });
422
+ expect(labels).toEqual(["Col1", "Col2"]);
423
+ });
424
+
425
+ test("linkerPath falls back to Label when LinkLabel is absent", () => {
426
+ const entries: Entry[] = [
427
+ {
428
+ spec: createSpec({
429
+ annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col1" }]) },
430
+ }),
431
+ linkerPath: [{ spec: createSpec({ annotations: { [Annotation.Label]: "FallbackLabel" } }) }],
432
+ },
433
+ {
434
+ spec: createSpec({
435
+ annotations: { [Annotation.Trace]: JSON.stringify([{ type: "t1", label: "Col2" }]) },
436
+ }),
437
+ },
438
+ ];
439
+ const labels = deriveDistinctLabels(entries);
440
+ expect(labels).toEqual(["Col1 via FallbackLabel", "Col2"]);
343
441
  });
344
442
 
345
443
  // --- addLabelAsSuffix ---
@@ -349,7 +447,7 @@ test("addLabelAsSuffix places native label at the end", () => {
349
447
  const labels = deriveDistinctLabels(specs, {
350
448
  includeNativeLabel: true,
351
449
  addLabelAsSuffix: true,
352
- }).map((r) => r.label);
450
+ });
353
451
  expect(labels).toEqual(["L1 / Label", "L2 / Label"]);
354
452
  });
355
453
 
@@ -370,7 +468,7 @@ test("custom separator is used between label parts", () => {
370
468
  { type: "t2", label: "Y" },
371
469
  ],
372
470
  ]);
373
- const labels = deriveDistinctLabels(specs, { separator: " - " }).map((r) => r.label);
471
+ const labels = deriveDistinctLabels(specs, { separator: " - " });
374
472
  expect(labels).toEqual(["A - X", "A - Y", "B - Y"]);
375
473
  });
376
474
 
@@ -378,7 +476,7 @@ test("custom separator is used between label parts", () => {
378
476
 
379
477
  test("single value gets its trace label", () => {
380
478
  const specs = tracesToSpecs([[{ type: "t1", label: "Only" }]]);
381
- const labels = deriveDistinctLabels(specs).map((r) => r.label);
479
+ const labels = deriveDistinctLabels(specs);
382
480
  expect(labels).toEqual(["Only"]);
383
481
  });
384
482
 
@@ -395,13 +493,13 @@ test("Unlabeled fallback when no trace entries match", () => {
395
493
  delete spec.annotations![Annotation.Label];
396
494
 
397
495
  const result = deriveDistinctLabels([spec, spec]);
398
- expect(result.every((r) => r.label === "Same")).toBe(true);
496
+ expect(result.every((r) => r === "Same")).toBe(true);
399
497
  });
400
498
 
401
499
  test("Unlabeled when no traces and no label", () => {
402
500
  const spec = createSpec();
403
501
  const result = deriveDistinctLabels([spec, spec]);
404
- expect(result.every((r) => r.label === "Unlabeled")).toBe(true);
502
+ expect(result.every((r) => r === "Unlabeled")).toBe(true);
405
503
  });
406
504
 
407
505
  // --- repeated type occurrences (secondaryTypes path) ---
@@ -418,7 +516,7 @@ test("repeated type occurrences are used as secondary types", () => {
418
516
  { type: "t1", label: "B" },
419
517
  ],
420
518
  ]);
421
- const labels = deriveDistinctLabels(specs).map((r) => r.label);
519
+ const labels = deriveDistinctLabels(specs);
422
520
  // t1@1 has label "First" for both (same), t1@2 has "A" vs "B" (distinguishing)
423
521
  // t1@2 is secondary since it only appears when there are 2 occurrences
424
522
  expect(labels).toEqual(["A", "B"]);
@@ -439,7 +537,7 @@ test("spec without native label uses only trace entries", () => {
439
537
  },
440
538
  }),
441
539
  ];
442
- const labels = deriveDistinctLabels(specs).map((r) => r.label);
540
+ const labels = deriveDistinctLabels(specs);
443
541
  expect(labels).toEqual(["X", "Y"]);
444
542
  });
445
543
 
@@ -456,6 +554,6 @@ test("includeNativeLabel with no native label does not break", () => {
456
554
  },
457
555
  }),
458
556
  ];
459
- const labels = deriveDistinctLabels(specs, { includeNativeLabel: true }).map((r) => r.label);
557
+ const labels = deriveDistinctLabels(specs, { includeNativeLabel: true });
460
558
  expect(labels).toEqual(["X", "Y"]);
461
559
  });
@@ -2,32 +2,34 @@ import {
2
2
  Annotation,
3
3
  parseJson,
4
4
  readAnnotation,
5
- type CanonicalizedJson,
6
5
  type PObjectSpec,
6
+ type StringifiedJson,
7
+ type Trace,
7
8
  } from "@milaboratories/pl-model-common";
8
9
  import { throwError } from "@milaboratories/helpers";
10
+ import { isFunction, isNil } from "es-toolkit";
11
+
12
+ export type { Trace, TraceEntry } from "@milaboratories/pl-model-common";
9
13
 
10
14
  const DISTANCE_PENALTY = 0.001;
11
15
  const LABEL_TYPE = "__LABEL__";
12
16
  const LABEL_TYPE_FULL = "__LABEL__@1";
13
17
 
14
- export type WithLabel<T> = {
15
- value: T;
16
- label: string;
17
- };
18
-
19
- type TraceEntry = {
20
- id?: string;
21
- type: string;
22
- label: string;
18
+ /** SDK-internal trace shape — adds fields used by this algorithm only, not part of the on-disk contract. */
19
+ type ExtendedTraceEntry = Trace[number] & {
23
20
  importance?: number;
21
+ position?: "prefix" | "suffix";
24
22
  };
25
23
 
26
- export type Trace = TraceEntry[];
27
-
28
24
  export type Entry =
29
25
  | PObjectSpec
30
- | { spec: PObjectSpec; prefixTrace?: TraceEntry[]; suffixTrace?: TraceEntry[] };
26
+ | {
27
+ spec: PObjectSpec;
28
+ /** Extra trace entries merged with the base trace from annotations. */
29
+ extraTrace?: ExtendedTraceEntry[];
30
+ /** Linker steps traversed to discover this column; used to append "via $linkLabel" to derived labels. */
31
+ linkerPath?: { spec: PObjectSpec }[];
32
+ };
31
33
 
32
34
  export type DeriveLabelsOptions = {
33
35
  /** Separator to use between label parts (" / " by default) */
@@ -38,18 +40,33 @@ export type DeriveLabelsOptions = {
38
40
  includeNativeLabel?: boolean;
39
41
  /** Trace elements list that will be forced to be included in the label. */
40
42
  forceTraceElements?: string[];
43
+ /** Custom formatter for linker path suffix. Receives the array of linker labels from the full traversal chain,
44
+ * the column spec, and the column index.
45
+ * If returns undefined, no linker suffix is appended. By default labels are joined with " > " and prefixed with "via ". */
46
+ linkerLabelFormatter?: (
47
+ linkerLabels: string[],
48
+ spec: PObjectSpec,
49
+ index: number,
50
+ ) => string | undefined;
41
51
  };
42
52
 
43
- export function deriveDistinctLabels<T extends Entry>(
44
- values: T[],
45
- options: DeriveLabelsOptions = {},
46
- ): WithLabel<T>[] {
53
+ export function deriveDistinctLabels(values: Entry[], options: DeriveLabelsOptions = {}): string[] {
47
54
  const forceTraceElements =
48
55
  options.forceTraceElements !== undefined && options.forceTraceElements.length > 0
49
56
  ? new Set(options.forceTraceElements)
50
57
  : undefined;
51
58
  const separator = options.separator ?? " / ";
52
59
 
60
+ // Collect per-entry linker suffixes before disambiguation
61
+ const linkerSuffixes = values.map((v, i) => {
62
+ const spec = "spec" in v && typeof v.spec === "object" ? v.spec : (v as PObjectSpec);
63
+ const linkerLabels = extractLinkerLabels(v);
64
+ if (linkerLabels.length === 0) return undefined;
65
+ return isFunction(options.linkerLabelFormatter)
66
+ ? options.linkerLabelFormatter(linkerLabels, spec, i)
67
+ : `via ${linkerLabels.join(" > ")}`;
68
+ });
69
+
53
70
  // Phase 1: enrich each value with parsed trace
54
71
  const records = values.map((v) => enrichRecord(v, options));
55
72
 
@@ -65,7 +82,12 @@ export function deriveDistinctLabels<T extends Entry>(
65
82
  if (mainTypes.length === 0) {
66
83
  if (secondaryTypes.length !== 0)
67
84
  throw new Error("Non-empty secondary types list while main types list is empty.");
68
- return build(new Set(LABEL_TYPE_FULL), true)!;
85
+
86
+ return applyLinkerSuffixes(
87
+ build(new Set(LABEL_TYPE_FULL), true) ??
88
+ throwError("Failed to derive labels using native column labels"),
89
+ linkerSuffixes,
90
+ );
69
91
  }
70
92
 
71
93
  // Phase 4: search for minimal type set that produces unique labels
@@ -96,7 +118,10 @@ export function deriveDistinctLabels<T extends Entry>(
96
118
  options,
97
119
  separator,
98
120
  );
99
- return build(minimized, false) ?? throwError("Failed to derive unique labels");
121
+ return applyLinkerSuffixes(
122
+ build(minimized, false) ?? throwError("Failed to derive unique labels"),
123
+ linkerSuffixes,
124
+ );
100
125
  }
101
126
 
102
127
  additionalType++;
@@ -116,29 +141,55 @@ export function deriveDistinctLabels<T extends Entry>(
116
141
  options,
117
142
  separator,
118
143
  );
119
- return build(minimized, true) ?? throwError("Failed to derive unique labels");
144
+ return applyLinkerSuffixes(
145
+ build(minimized, true) ?? throwError("Failed to derive unique labels"),
146
+ linkerSuffixes,
147
+ );
148
+ }
149
+
150
+ /** Apply pre-formatted linker suffixes to labels that have them. */
151
+ function applyLinkerSuffixes(labels: string[], suffixes: (string | undefined)[]): string[] {
152
+ return labels.map((label, i) => (isNil(suffixes[i]) ? label : `${label} ${suffixes[i]}`));
153
+ }
154
+
155
+ /** Extract linker labels from every step of the linkers path. */
156
+ function extractLinkerLabels(entry: Entry): string[] {
157
+ if (!("spec" in entry) || typeof entry.spec !== "object") return [];
158
+ const path = entry.linkerPath;
159
+ if (path === undefined || path.length === 0) return [];
160
+ const labels: string[] = [];
161
+ for (const step of path) {
162
+ const label = (
163
+ readAnnotation(step.spec, Annotation.LinkLabel) ?? readAnnotation(step.spec, Annotation.Label)
164
+ )?.trim();
165
+ if (label !== undefined && label.length > 0) {
166
+ labels.push(label);
167
+ }
168
+ }
169
+ return labels;
120
170
  }
121
171
 
122
172
  // --- Pure helpers ---
123
- type FullTraceEntry = TraceEntry & { fullType: string; occurrenceIndex: number };
173
+ type FullTraceEntry = ExtendedTraceEntry & { fullType: string; occurrenceIndex: number };
124
174
 
125
- type EnrichedRecord<T> = {
126
- value: T;
175
+ type EnrichedRecord = {
127
176
  fullTrace: FullTraceEntry[];
128
177
  };
129
178
 
130
179
  function extractSpecAndTrace(entry: Entry): {
131
180
  spec: PObjectSpec;
132
- prefixTrace: TraceEntry[] | undefined;
133
- suffixTrace: TraceEntry[] | undefined;
181
+ extraTrace: ExtendedTraceEntry[] | undefined;
182
+ linkerPath: { spec: PObjectSpec }[] | undefined;
134
183
  } {
135
- if ("spec" in entry && typeof entry.spec === "object") {
136
- return { spec: entry.spec, prefixTrace: entry.prefixTrace, suffixTrace: entry.suffixTrace };
137
- }
138
- return { spec: entry as PObjectSpec, prefixTrace: undefined, suffixTrace: undefined };
184
+ const isEnriched = "spec" in entry && typeof entry.spec === "object";
185
+ return {
186
+ spec: isEnriched ? entry.spec : (entry as PObjectSpec),
187
+ extraTrace: isEnriched ? entry.extraTrace : undefined,
188
+ linkerPath: isEnriched ? entry.linkerPath : undefined,
189
+ };
139
190
  }
140
191
 
141
- function buildFullTrace(trace: TraceEntry[]): FullTraceEntry[] {
192
+ function buildFullTrace(trace: ExtendedTraceEntry[]): FullTraceEntry[] {
142
193
  const result: FullTraceEntry[] = [];
143
194
  const occurrences = new Map<string, number>();
144
195
 
@@ -157,15 +208,17 @@ function buildFullTrace(trace: TraceEntry[]): FullTraceEntry[] {
157
208
  return result;
158
209
  }
159
210
 
160
- function enrichRecord<T extends Entry>(value: T, options: DeriveLabelsOptions): EnrichedRecord<T> {
161
- const { spec, prefixTrace, suffixTrace } = extractSpecAndTrace(value);
211
+ function enrichRecord(value: Entry, options: DeriveLabelsOptions): EnrichedRecord {
212
+ const { spec, extraTrace } = extractSpecAndTrace(value);
162
213
 
163
214
  const label = readAnnotation(spec, Annotation.Label);
164
- const traceStr = readAnnotation(spec, Annotation.Trace) as
165
- | CanonicalizedJson<TraceEntry[]>
166
- | undefined;
167
- const baseTrace: Trace = traceStr ? (parseJson<Trace>(traceStr) ?? []) : [];
168
- const trace = [...(prefixTrace ?? []), ...baseTrace, ...(suffixTrace ?? [])];
215
+ const traceStr = readAnnotation(spec, Annotation.Trace);
216
+ const baseTrace = traceStr
217
+ ? (parseJson(traceStr as StringifiedJson<ExtendedTraceEntry[]>) ?? [])
218
+ : [];
219
+ const prefixExtra = extraTrace?.filter((e) => e.position === "prefix") ?? [];
220
+ const suffixExtra = extraTrace?.filter((e) => e.position !== "prefix") ?? [];
221
+ const trace = [...prefixExtra, ...baseTrace, ...suffixExtra];
169
222
 
170
223
  if (label !== undefined) {
171
224
  const labelEntry = { label, type: LABEL_TYPE, importance: -2 };
@@ -173,7 +226,7 @@ function enrichRecord<T extends Entry>(value: T, options: DeriveLabelsOptions):
173
226
  else trace.splice(0, 0, labelEntry);
174
227
  }
175
228
 
176
- return { value, fullTrace: buildFullTrace(trace) };
229
+ return { fullTrace: buildFullTrace(trace) };
177
230
  }
178
231
 
179
232
  type TypeStats = {
@@ -181,7 +234,7 @@ type TypeStats = {
181
234
  countByType: Map<string, number>;
182
235
  };
183
236
 
184
- function collectTypeStats<T>(records: EnrichedRecord<T>[]): TypeStats {
237
+ function collectTypeStats(records: EnrichedRecord[]): TypeStats {
185
238
  const importances = new Map<string, number>();
186
239
  const countByType = new Map<string, number>();
187
240
 
@@ -220,14 +273,14 @@ function classifyTypes(
220
273
  return { mainTypes, secondaryTypes };
221
274
  }
222
275
 
223
- function buildLabels<T>(
224
- records: EnrichedRecord<T>[],
276
+ function buildLabels(
277
+ records: EnrichedRecord[],
225
278
  includedTypes: Set<string>,
226
279
  forceTraceElements: Set<string> | undefined,
227
280
  separator: string,
228
281
  force: boolean,
229
- ): WithLabel<T>[] | undefined {
230
- const result: WithLabel<T>[] = [];
282
+ ): string[] | undefined {
283
+ const result: string[] = [];
231
284
 
232
285
  for (const r of records) {
233
286
  const parts: string[] = [];
@@ -239,24 +292,24 @@ function buildLabels<T>(
239
292
 
240
293
  if (parts.length === 0) {
241
294
  if (!force) return undefined;
242
- result.push({ label: "Unlabeled", value: r.value });
295
+ result.push("Unlabeled");
243
296
  continue;
244
297
  }
245
298
 
246
- result.push({ label: parts.join(separator), value: r.value });
299
+ result.push(parts.join(separator));
247
300
  }
248
301
 
249
302
  return result;
250
303
  }
251
304
 
252
- function countUniqueLabels<T>(result: WithLabel<T>[] | undefined): number {
305
+ function countUniqueLabels(result: string[] | undefined): number {
253
306
  if (result === undefined) return 0;
254
- return new Set(result.map((c) => c.label)).size;
307
+ return new Set(result).size;
255
308
  }
256
309
 
257
- function minimizeTypeSet<T>(
310
+ function minimizeTypeSet(
258
311
  typeSet: Set<string>,
259
- records: EnrichedRecord<T>[],
312
+ records: EnrichedRecord[],
260
313
  stats: TypeStats,
261
314
  forceTraceElements: Set<string> | undefined,
262
315
  options: DeriveLabelsOptions,
@@ -1,2 +1 @@
1
1
  export * from "./derive_distinct_labels";
2
- export * from "./write_labels_to_specs";