@platforma-sdk/model 1.65.9 → 1.66.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 (121) hide show
  1. package/dist/block_model.cjs +8 -11
  2. package/dist/block_model.cjs.map +1 -1
  3. package/dist/block_model.d.ts.map +1 -1
  4. package/dist/block_model.js +8 -10
  5. package/dist/block_model.js.map +1 -1
  6. package/dist/columns/column_collection_builder.cjs +61 -74
  7. package/dist/columns/column_collection_builder.cjs.map +1 -1
  8. package/dist/columns/column_collection_builder.d.ts +16 -22
  9. package/dist/columns/column_collection_builder.d.ts.map +1 -1
  10. package/dist/columns/column_collection_builder.js +62 -75
  11. package/dist/columns/column_collection_builder.js.map +1 -1
  12. package/dist/columns/column_selector.cjs.map +1 -1
  13. package/dist/columns/column_selector.d.ts +1 -1
  14. package/dist/columns/column_selector.js.map +1 -1
  15. package/dist/columns/column_snapshot.cjs.map +1 -1
  16. package/dist/columns/column_snapshot.d.ts +4 -4
  17. package/dist/columns/column_snapshot.d.ts.map +1 -1
  18. package/dist/columns/column_snapshot.js.map +1 -1
  19. package/dist/columns/ctx_column_sources.cjs.map +1 -1
  20. package/dist/columns/ctx_column_sources.d.ts +1 -1
  21. package/dist/columns/ctx_column_sources.d.ts.map +1 -1
  22. package/dist/columns/ctx_column_sources.js.map +1 -1
  23. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs +2 -2
  24. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs.map +1 -1
  25. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js +2 -2
  26. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js.map +1 -1
  27. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.cjs +17 -18
  28. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.cjs.map +1 -1
  29. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.js +17 -18
  30. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.js.map +1 -1
  31. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs +99 -91
  32. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -1
  33. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts +16 -16
  34. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts.map +1 -1
  35. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js +102 -94
  36. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -1
  37. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs +32 -23
  38. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs.map +1 -1
  39. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts +5 -5
  40. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts.map +1 -1
  41. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js +33 -24
  42. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js.map +1 -1
  43. package/dist/components/PlDataTable/createPlDataTable/index.cjs.map +1 -1
  44. package/dist/components/PlDataTable/createPlDataTable/index.d.ts +2 -3
  45. package/dist/components/PlDataTable/createPlDataTable/index.d.ts.map +1 -1
  46. package/dist/components/PlDataTable/createPlDataTable/index.js.map +1 -1
  47. package/dist/components/PlDataTable/createPlDataTable/utils.cjs +133 -16
  48. package/dist/components/PlDataTable/createPlDataTable/utils.cjs.map +1 -1
  49. package/dist/components/PlDataTable/createPlDataTable/utils.d.ts +8 -6
  50. package/dist/components/PlDataTable/createPlDataTable/utils.d.ts.map +1 -1
  51. package/dist/components/PlDataTable/createPlDataTable/utils.js +130 -17
  52. package/dist/components/PlDataTable/createPlDataTable/utils.js.map +1 -1
  53. package/dist/components/PlDataTable/labels.cjs +1 -2
  54. package/dist/components/PlDataTable/labels.cjs.map +1 -1
  55. package/dist/components/PlDataTable/labels.js +1 -2
  56. package/dist/components/PlDataTable/labels.js.map +1 -1
  57. package/dist/filters/distill.cjs +73 -30
  58. package/dist/filters/distill.cjs.map +1 -1
  59. package/dist/filters/distill.d.ts.map +1 -1
  60. package/dist/filters/distill.js +73 -30
  61. package/dist/filters/distill.js.map +1 -1
  62. package/dist/index.cjs +19 -15
  63. package/dist/index.d.ts +4 -2
  64. package/dist/index.js +6 -4
  65. package/dist/labels/derive_distinct_tooltips.cjs +85 -0
  66. package/dist/labels/derive_distinct_tooltips.cjs.map +1 -0
  67. package/dist/labels/derive_distinct_tooltips.d.ts +17 -0
  68. package/dist/labels/derive_distinct_tooltips.d.ts.map +1 -0
  69. package/dist/labels/derive_distinct_tooltips.js +84 -0
  70. package/dist/labels/derive_distinct_tooltips.js.map +1 -0
  71. package/dist/labels/index.cjs +1 -0
  72. package/dist/labels/index.d.ts +2 -1
  73. package/dist/labels/index.js +1 -0
  74. package/dist/package.cjs +1 -1
  75. package/dist/package.js +1 -1
  76. package/dist/render/api.cjs +8 -13
  77. package/dist/render/api.cjs.map +1 -1
  78. package/dist/render/api.d.ts +8 -11
  79. package/dist/render/api.d.ts.map +1 -1
  80. package/dist/render/api.js +8 -13
  81. package/dist/render/api.js.map +1 -1
  82. package/dist/services/get_services.cjs +19 -0
  83. package/dist/services/get_services.cjs.map +1 -0
  84. package/dist/services/get_services.d.ts +7 -0
  85. package/dist/services/get_services.d.ts.map +1 -0
  86. package/dist/services/get_services.js +19 -0
  87. package/dist/services/get_services.js.map +1 -0
  88. package/dist/services/index.cjs +1 -0
  89. package/dist/services/index.d.ts +2 -1
  90. package/dist/services/index.js +1 -0
  91. package/dist/services/service_bridge.cjs +4 -4
  92. package/dist/services/service_bridge.cjs.map +1 -1
  93. package/dist/services/service_bridge.d.ts +4 -4
  94. package/dist/services/service_bridge.d.ts.map +1 -1
  95. package/dist/services/service_bridge.js +4 -4
  96. package/dist/services/service_bridge.js.map +1 -1
  97. package/package.json +6 -6
  98. package/src/block_model.ts +8 -11
  99. package/src/columns/column_collection_builder.test.ts +75 -30
  100. package/src/columns/column_collection_builder.ts +96 -133
  101. package/src/columns/column_selector.ts +1 -1
  102. package/src/columns/column_snapshot.ts +7 -4
  103. package/src/columns/ctx_column_sources.ts +1 -3
  104. package/src/components/PFrameForGraphs.test.ts +4 -4
  105. package/src/components/PlDataTable/createPlDataTable/createPTableDefV2.ts +2 -2
  106. package/src/components/PlDataTable/createPlDataTable/createPTableDefV3.ts +44 -21
  107. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts +202 -218
  108. package/src/components/PlDataTable/createPlDataTable/discoverColumns.ts +69 -56
  109. package/src/components/PlDataTable/createPlDataTable/index.ts +6 -7
  110. package/src/components/PlDataTable/createPlDataTable/utils.test.ts +97 -1
  111. package/src/components/PlDataTable/createPlDataTable/utils.ts +190 -35
  112. package/src/components/PlDataTable/labels.ts +3 -7
  113. package/src/filters/distill.test.ts +91 -0
  114. package/src/filters/distill.ts +102 -46
  115. package/src/labels/derive_distinct_tooltips.test.ts +233 -0
  116. package/src/labels/derive_distinct_tooltips.ts +130 -0
  117. package/src/labels/index.ts +1 -0
  118. package/src/render/api.ts +15 -50
  119. package/src/services/get_services.ts +28 -0
  120. package/src/services/index.ts +1 -0
  121. package/src/services/service_bridge.ts +5 -5
@@ -1,34 +1,24 @@
1
1
  import type {
2
2
  AxisQualification,
3
- ColumnAxesWithQualifications,
4
3
  DiscoverColumnsConstraints,
5
4
  DiscoverColumnsRequest,
6
5
  DiscoverColumnsResponse,
7
6
  MultiColumnSelector,
8
7
  NativePObjectId,
9
- PColumnIdAndSpec,
10
8
  PColumnSpec,
11
9
  PObjectId,
12
- SUniversalPColumnId,
13
- } from "@milaboratories/pl-model-common";
14
- import {
15
- AnchoredIdDeriver,
16
- canonicalizeJson,
17
- deriveNativeId,
18
- getAxesId,
19
- isPColumnSpec,
20
10
  } from "@milaboratories/pl-model-common";
11
+ import { deriveNativeId, isPColumnSpec } from "@milaboratories/pl-model-common";
21
12
  import type { ColumnSelector, RelaxedColumnSelector } from "./column_selector";
22
13
  import { convertColumnSelectorToMultiColumnSelector } from "./column_selector";
23
14
  import { TreeNodeAccessor } from "../render/accessor";
24
15
  import type { ColumnSnapshot } from "./column_snapshot";
25
- import { createColumnSnapshot } from "./column_snapshot";
26
16
  import type { ColumnSnapshotProvider, ColumnSource } from "./column_snapshot_provider";
27
17
  import { ArrayColumnProvider, toColumnSnapshotProvider } from "./column_snapshot_provider";
28
18
 
29
19
  import type { PFrameSpecDriver, PoolEntry, SpecFrameHandle } from "@milaboratories/pl-model-common";
30
20
  import { throwError } from "@milaboratories/helpers";
31
- import { uniqBy } from "es-toolkit";
21
+ import { getService } from "../services";
32
22
 
33
23
  // --- FindColumnsOptions ---
34
24
 
@@ -47,9 +37,6 @@ export interface ColumnCollection extends Disposable {
47
37
  /** Release the underlying spec frame WASM resource. */
48
38
  dispose(): void;
49
39
 
50
- /** Point lookup by provider-native ID. */
51
- getColumn(id: PObjectId): undefined | ColumnSnapshot<PObjectId>;
52
-
53
40
  /** Find columns matching selectors. Returns flat list of snapshots.
54
41
  * No axis compatibility matching, no linker traversal.
55
42
  * Never returns undefined — the "not ready" state was absorbed by the builder. */
@@ -64,10 +51,7 @@ export interface AnchoredColumnCollection extends Disposable {
64
51
  dispose(): void;
65
52
 
66
53
  /** List of anchors used for discovery, with their resolved specs. */
67
- getAnchors(): Map<string, PColumnIdAndSpec>;
68
-
69
- /** Point lookup by anchored ID. */
70
- getColumn(id: SUniversalPColumnId): undefined | ColumnSnapshot<SUniversalPColumnId>;
54
+ getAnchors(): Map<string, ColumnSnapshot<PObjectId>>;
71
55
 
72
56
  /** Axis-aware column discovery. */
73
57
  findColumns(options?: AnchoredFindColumnsOptions): ColumnMatch[];
@@ -87,34 +71,32 @@ export interface AnchoredFindColumnsOptions extends FindColumnsOptions {
87
71
  /** Result of anchored discovery — column snapshot + routing info. */
88
72
  export interface ColumnMatch {
89
73
  /** Column snapshot with anchored SUniversalPColumnId. */
90
- readonly column: ColumnSnapshot<SUniversalPColumnId>;
91
- /** Provider-native IDfor lookups back to the source provider. */
92
- readonly originalId: PObjectId;
93
- /** Match variants — different paths/qualifications that reach this column. */
74
+ readonly column: ColumnSnapshot<PObjectId>;
75
+ /** Match variantsdifferent ways (paths/qualifications) to reach this column. */
94
76
  readonly variants: MatchVariant[];
77
+ }
78
+
79
+ /** A single mapping variant describing how a hit column can be integrated. */
80
+ export interface MatchVariant {
81
+ /** Full qualifications needed for integration. */
82
+ readonly qualifications: MatchQualifications;
83
+ /** Distinctive (minimal) qualifications needed for integration. */
84
+ readonly distinctiveQualifications: MatchQualifications;
95
85
  /** Linker steps traversed to reach this hit; empty for direct matches. */
96
86
  readonly path: {
97
- linker: ColumnSnapshot<SUniversalPColumnId>;
87
+ linker: ColumnSnapshot<PObjectId>;
98
88
  qualifications: AxisQualification[];
99
89
  }[];
100
90
  }
101
91
 
102
- /** Qualifications needed for both query (already-integrated) columns and the hit column. */
92
+ /** Qualifications needed for both already-integrated anchor columns and the hit column. */
103
93
  export interface MatchQualifications {
104
- /** Qualifications for each query (already-integrated) column set. */
105
- readonly forQueries: AxisQualification[][];
94
+ /** Qualifications for already-integrated anchor columns */
95
+ readonly forQueries: Record<PObjectId, AxisQualification[]>;
106
96
  /** Qualifications for the hit column. */
107
97
  readonly forHit: AxisQualification[];
108
98
  }
109
99
 
110
- /** A single mapping variant describing how a hit column can be integrated. */
111
- export interface MatchVariant {
112
- /** Full qualifications needed for integration. */
113
- readonly qualifications: MatchQualifications;
114
- /** Distinctive (minimal) qualifications needed for integration. */
115
- readonly distinctiveQualifications: MatchQualifications;
116
- }
117
-
118
100
  // --- Build options ---
119
101
 
120
102
  export interface BuildOptions {
@@ -139,7 +121,7 @@ export interface AnchoredBuildOptions extends BuildOptions {
139
121
  export class ColumnCollectionBuilder {
140
122
  private readonly providers: ColumnSnapshotProvider[] = [];
141
123
 
142
- constructor(private readonly specDriver: PFrameSpecDriver) {}
124
+ constructor(private readonly specDriver: PFrameSpecDriver = getService("pframeSpec")) {}
143
125
 
144
126
  /**
145
127
  * Register a column source. Sources added first take precedence for dedup.
@@ -232,12 +214,6 @@ class ColumnCollectionImpl implements ColumnCollection, Disposable {
232
214
  this.dispose();
233
215
  }
234
216
 
235
- getColumn(id: PObjectId): undefined | ColumnSnapshot<PObjectId> {
236
- const col = this.columns.get(id);
237
- if (col === undefined) return undefined;
238
- return this.toSnapshot(col);
239
- }
240
-
241
217
  findColumns(options?: FindColumnsOptions): ColumnSnapshot<PObjectId>[] {
242
218
  const includeColumns = options?.include ? toMultiColumnSelectors(options.include) : undefined;
243
219
  const excludeColumns = options?.exclude ? toMultiColumnSelectors(options.exclude) : undefined;
@@ -253,15 +229,10 @@ class ColumnCollectionImpl implements ColumnCollection, Disposable {
253
229
  // Map hits back to snapshots
254
230
  const results = response.hits
255
231
  .map((hit) => this.columns.get(hit.hit.columnId as PObjectId))
256
- .filter((col): col is ColumnSnapshot<PObjectId> => col !== undefined)
257
- .map((col) => this.toSnapshot(col));
232
+ .filter((col): col is ColumnSnapshot<PObjectId> => col !== undefined);
258
233
 
259
234
  return results;
260
235
  }
261
-
262
- private toSnapshot(col: ColumnSnapshot<PObjectId>): ColumnSnapshot<PObjectId> {
263
- return remapSnapshot(col.id, col);
264
- }
265
236
  }
266
237
 
267
238
  // --- AnchoredColumnCollectionImpl ---
@@ -271,12 +242,8 @@ interface AnchoredColumnCollectionImplOptions extends ColumnCollectionImplOption
271
242
  }
272
243
 
273
244
  class AnchoredColumnCollectionImpl implements AnchoredColumnCollection, Disposable {
274
- private readonly anchorsMap: Map<string, PColumnIdAndSpec>;
245
+ private readonly anchorsMap: Map<string, ColumnSnapshot<PObjectId>>;
275
246
  private readonly columnsMap: Map<PObjectId, ColumnSnapshot<PObjectId>>;
276
-
277
- private readonly idDeriver: AnchoredIdDeriver;
278
- private readonly uniqAnchorAxes: ColumnAxesWithQualifications[];
279
- private readonly idToOriginalIdMap: Map<SUniversalPColumnId, PObjectId>;
280
247
  private readonly specFrameEntry: PoolEntry<SpecFrameHandle>;
281
248
 
282
249
  constructor(
@@ -293,21 +260,6 @@ class AnchoredColumnCollectionImpl implements AnchoredColumnCollection, Disposab
293
260
  options.columns,
294
261
  this.specDriver.discoverColumns.bind(this.specDriver, this.specFrameEntry.key),
295
262
  );
296
- this.idDeriver = new AnchoredIdDeriver(
297
- Object.fromEntries(
298
- Array.from(this.anchorsMap.entries()).map(([k, v]) => [k, v.spec] as const),
299
- ),
300
- );
301
- this.uniqAnchorAxes = uniqBy(
302
- Array.from(this.anchorsMap.values(), ({ spec }) => ({
303
- axesSpec: spec.axesSpec,
304
- qualifications: [],
305
- })),
306
- (axis) => canonicalizeJson(getAxesId(axis.axesSpec)) + canonicalizeJson(axis.qualifications),
307
- );
308
- this.idToOriginalIdMap = new Map(
309
- options.columns.map((col) => [this.idDeriver.deriveS(col.spec), col.id] as const),
310
- );
311
263
  }
312
264
 
313
265
  dispose(): void {
@@ -318,64 +270,60 @@ class AnchoredColumnCollectionImpl implements AnchoredColumnCollection, Disposab
318
270
  this.dispose();
319
271
  }
320
272
 
321
- getAnchors(): Map<string, PColumnIdAndSpec> {
273
+ getAnchors(): Map<string, ColumnSnapshot<PObjectId>> {
322
274
  return this.anchorsMap;
323
275
  }
324
276
 
325
- getColumn(id: SUniversalPColumnId): undefined | ColumnSnapshot<SUniversalPColumnId> {
326
- const origId = this.idToOriginalIdMap.get(id);
327
- if (origId === undefined) return undefined;
328
- const col = this.columnsMap.get(origId);
329
- if (col === undefined) return undefined;
330
- return remapSnapshot(id, col);
331
- }
332
-
333
277
  findColumns(options?: AnchoredFindColumnsOptions): ColumnMatch[] {
334
278
  const mode = options?.mode ?? "enrichment";
335
279
  const constraints = matchingModeToConstraints(mode);
336
280
  const includeColumns = options?.include ? toMultiColumnSelectors(options.include) : undefined;
337
281
  const excludeColumns = options?.exclude ? toMultiColumnSelectors(options.exclude) : undefined;
338
-
282
+ const anchors = Array.from(this.anchorsMap.values());
339
283
  const response = this.specDriver.discoverColumns(this.specFrameEntry.key, {
340
284
  includeColumns,
341
285
  excludeColumns,
342
286
  constraints,
343
287
  maxHops: options?.maxHops ?? 4,
344
- axes: this.uniqAnchorAxes,
288
+ axes: anchors.map((anchor) => ({
289
+ axesSpec: anchor.spec.axesSpec,
290
+ qualifications: [],
291
+ })),
345
292
  });
346
293
 
347
- // Map every WASM discovery hit to a ColumnMatch.
348
- // The same physical column may appear multiple times when reachable through
349
- // different linker paths — each hit becomes a separate ColumnMatch so that
350
- // the caller can expand them into distinct table columns.
351
- const results: ColumnMatch[] = [];
352
- for (const hit of response.hits) {
294
+ const byColumn = response.hits.reduce<Map<PObjectId, ColumnMatch>>((acc, hit) => {
353
295
  const origId = hit.hit.columnId as PObjectId;
354
296
  const col =
355
297
  this.columnsMap.get(origId) ??
356
298
  throwError(`Column with id ${origId} not found in collection`);
357
- const associatedId = this.idDeriver.deriveS(col.spec);
358
-
359
- results.push({
360
- path: hit.path.map((step) => {
361
- if (step.type !== "linker")
362
- throw new Error(`Unexpected discover-columns step type: ${step.type}`);
363
- return {
364
- linker: remapSnapshot(
365
- this.idDeriver.deriveS(step.linker.spec),
366
- this.columnsMap.get(step.linker.columnId) ??
367
- throwError(`Linker column with id ${step.linker.columnId} not found in collection`),
368
- ),
369
- qualifications: step.qualifications,
370
- };
371
- }),
372
- column: remapSnapshot(associatedId, col),
373
- variants: hit.mappingVariants,
374
- originalId: origId,
375
- });
376
- }
377
299
 
378
- return results;
300
+ const path = hit.path.map((step) => {
301
+ if (step.type !== "linker") {
302
+ throw new Error(`Unexpected discover-columns step type: ${step.type}`);
303
+ }
304
+
305
+ return {
306
+ linker:
307
+ this.columnsMap.get(step.linker.columnId) ??
308
+ throwError(`Linker column with id ${step.linker.columnId} not found in collection`),
309
+ qualifications: step.qualifications,
310
+ };
311
+ });
312
+ const variants: MatchVariant[] = hit.mappingVariants.map((v) => ({
313
+ path,
314
+ qualifications: remapFromIdxToId(v.qualifications, anchors),
315
+ distinctiveQualifications: remapFromIdxToId(v.distinctiveQualifications, anchors),
316
+ }));
317
+ const existing = acc.get(origId);
318
+ return acc.set(
319
+ origId,
320
+ existing === undefined
321
+ ? { column: col, variants }
322
+ : { ...existing, variants: [...existing.variants, ...variants] },
323
+ );
324
+ }, new Map());
325
+
326
+ return Array.from(byColumn.values());
379
327
  }
380
328
  }
381
329
 
@@ -402,15 +350,7 @@ function collectColumns(providers: ColumnSnapshotProvider[]): ColumnSnapshot<POb
402
350
 
403
351
  // --- Shared snapshot helpers ---
404
352
 
405
- /** Create a new snapshot with a different ID, preserving data accessors. */
406
- function remapSnapshot<Id extends PObjectId>(
407
- id: Id,
408
- col: ColumnSnapshot<PObjectId>,
409
- ): ColumnSnapshot<Id> {
410
- return createColumnSnapshot(id, col.spec, col.data, col.dataStatus);
411
- }
412
-
413
- /** Normalize SDK ColumnSelectorInput to MultiColumnSelector[]. */
353
+ /** Normalize ColumnSelector (relaxed, single or array) to MultiColumnSelector[]. */
414
354
  function toMultiColumnSelectors(input: ColumnSelector): MultiColumnSelector[] {
415
355
  return convertColumnSelectorToMultiColumnSelector(input);
416
356
  }
@@ -418,40 +358,44 @@ function toMultiColumnSelectors(input: ColumnSelector): MultiColumnSelector[] {
418
358
  // --- Anchor resolution ---
419
359
 
420
360
  /**
421
- * Resolve each anchor value to a PColumnSpec.
422
- * - PColumnSpec: used directly
423
- * - PObjectId (string): looked up in the collected column map
361
+ * Resolve each anchor entry to a ColumnSnapshot from the collected columns.
362
+ * - PObjectId (string): looked up by id in the collected columns
363
+ * - PColumnSpec: matched by deriveNativeId against collected columns
364
+ * - RelaxedColumnSelector: resolved via discoverColumns in "exact" mode;
365
+ * must match exactly one column
366
+ * Throws on unresolved, ambiguous, or duplicated matches. Requires at least one
367
+ * anchor to resolve.
424
368
  */
425
369
  function resolveAnchorMap(
426
370
  anchors: Record<string, AnchorEntry>,
427
371
  columns: ColumnSnapshot<PObjectId>[],
428
372
  discoverColumns: (request: DiscoverColumnsRequest) => DiscoverColumnsResponse,
429
- ): Map<string, PColumnIdAndSpec> {
430
- const result = new Map<string, PColumnIdAndSpec>();
373
+ ): Map<string, ColumnSnapshot<PObjectId>> {
374
+ const result = new Map<string, ColumnSnapshot<PObjectId>>();
431
375
  const resovedIds = new Set<PObjectId>();
432
376
  const getDuplicateError = (key: string) =>
433
377
  `Anchor "${key}": selector matched a column that was already matched by another anchor; please refine the selector to match a different column`;
434
378
 
435
- for (const [key, anchor] of Object.entries(anchors)) {
379
+ for (const [name, anchor] of Object.entries(anchors)) {
436
380
  if (typeof anchor === "string") {
437
381
  const found =
438
382
  columns.find((col) => col.id === anchor) ??
439
- throwError(`Anchor "${key}": column with id "${anchor}" not found in sources`);
383
+ throwError(`Anchor "${name}": column with id "${anchor}" not found in sources`);
440
384
  if (resovedIds.has(found.id)) {
441
- throwError(getDuplicateError(key));
385
+ throwError(getDuplicateError(name));
442
386
  }
443
- result.set(key, { columnId: found.id, spec: found.spec });
387
+ result.set(name, found);
444
388
  resovedIds.add(found.id);
445
389
  } else if ("kind" in anchor) {
446
- if (!isPColumnSpec(anchor)) throwError(`Anchor "${key}": invalid PColumnSpec`);
390
+ if (!isPColumnSpec(anchor)) throwError(`Anchor "${name}": invalid PColumnSpec`);
447
391
  const nativeId = deriveNativeId(anchor);
448
392
  const found =
449
393
  columns.find((col) => deriveNativeId(col.spec) === nativeId) ??
450
- throwError(`Anchor "${key}": no column matching spec found in sources`);
394
+ throwError(`Anchor "${name}": no column matching spec found in sources`);
451
395
  if (resovedIds.has(found.id)) {
452
- throwError(getDuplicateError(key));
396
+ throwError(getDuplicateError(name));
453
397
  }
454
- result.set(key, { columnId: found.id, spec: anchor });
398
+ result.set(name, found);
455
399
  resovedIds.add(found.id);
456
400
  } else {
457
401
  const matched = discoverColumns({
@@ -461,20 +405,25 @@ function resolveAnchorMap(
461
405
  maxHops: 0,
462
406
  constraints: matchingModeToConstraints("exact"),
463
407
  });
408
+
464
409
  if (matched.hits.length === 0) {
465
- throwError(`Anchor "${key}": no columns matched selector`);
410
+ throwError(`Anchor "${name}": no columns matched selector`);
466
411
  }
467
412
  if (matched.hits.length > 1) {
468
413
  throwError(
469
- `Anchor "${key}": selector is ambiguous and matched multiple columns; please refine the selector to match exactly one column`,
414
+ `Anchor "${name}": selector is ambiguous and matched multiple columns; please refine the selector to match exactly one column`,
470
415
  );
471
416
  }
472
417
  if (resovedIds.has(matched.hits[0].hit.columnId as PObjectId)) {
473
- throwError(getDuplicateError(key));
418
+ throwError(getDuplicateError(name));
474
419
  }
475
420
 
476
- result.set(key, matched.hits[0].hit);
477
- resovedIds.add(matched.hits[0].hit.columnId);
421
+ const id = matched.hits[0].hit.columnId as PObjectId;
422
+ const snap =
423
+ columns.find((col) => col.id === id) ??
424
+ throwError(`Anchor "${name}": matched column with id "${id}" not found in sources`);
425
+ result.set(name, snap);
426
+ resovedIds.add(snap.id);
478
427
  }
479
428
  }
480
429
 
@@ -485,6 +434,20 @@ function resolveAnchorMap(
485
434
  return result;
486
435
  }
487
436
 
437
+ function remapFromIdxToId(
438
+ qualifications: {
439
+ forQueries: AxisQualification[][];
440
+ forHit: AxisQualification[];
441
+ },
442
+ anchors: ColumnSnapshot<PObjectId>[],
443
+ ): MatchQualifications {
444
+ const forQueries = qualifications.forQueries.reduce<Record<PObjectId, AxisQualification[]>>(
445
+ (acc, qs, i) => (anchors[i] ? ((acc[anchors[i].id] = qs), acc) : acc),
446
+ {},
447
+ );
448
+ return { forQueries, forHit: qualifications.forHit };
449
+ }
450
+
488
451
  // --- MatchingMode → DiscoverColumnsConstraints ---
489
452
 
490
453
  function matchingModeToConstraints(mode: MatchingMode): DiscoverColumnsConstraints {
@@ -36,7 +36,7 @@ export interface RelaxedColumnSelector {
36
36
  partialAxesMatch?: boolean;
37
37
  }
38
38
 
39
- /** Input that normalizes to ColumnSelector[]. */
39
+ /** One or many relaxed column selectors; normalizes to MultiColumnSelector[]. */
40
40
  export type ColumnSelector = RelaxedColumnSelector | RelaxedColumnSelector[];
41
41
 
42
42
  // --- Normalization ---
@@ -13,7 +13,10 @@ export type ColumnDataStatus = "ready" | "computing" | "absent";
13
13
  * - `data` holds an active object when data exists (ready or computing),
14
14
  * or `undefined` when data is permanently absent.
15
15
  */
16
- export interface ColumnSnapshot<Id extends PObjectId | SUniversalPColumnId> {
16
+ export interface ColumnSnapshot<
17
+ Id extends PObjectId | SUniversalPColumnId,
18
+ Data = PColumnDataUniversal,
19
+ > {
17
20
  readonly id: Id;
18
21
  readonly spec: PColumnSpec;
19
22
  readonly dataStatus: ColumnDataStatus;
@@ -24,7 +27,7 @@ export interface ColumnSnapshot<Id extends PObjectId | SUniversalPColumnId> {
24
27
  * - `'computing'`: `data.get()` returns `undefined`, marks context unstable.
25
28
  * - `'absent'`: `data` is `undefined` — no active object, no instability.
26
29
  */
27
- readonly data: ColumnData | undefined;
30
+ readonly data: ColumnData<Data> | undefined;
28
31
  }
29
32
 
30
33
  // --- ColumnData ---
@@ -33,8 +36,8 @@ export interface ColumnSnapshot<Id extends PObjectId | SUniversalPColumnId> {
33
36
  * Active object wrapping lazy column data access.
34
37
  * Accessing data on a computing column marks the render context unstable.
35
38
  */
36
- export interface ColumnData {
37
- get(): PColumnDataUniversal | undefined;
39
+ export interface ColumnData<Data = PColumnDataUniversal> {
40
+ get(): Data | undefined;
38
41
  }
39
42
 
40
43
  /** Creates a ColumnData active object for a ready column. */
@@ -17,9 +17,7 @@ import type { ValueOf } from "@milaboratories/helpers";
17
17
  *
18
18
  * Returns an array of providers suitable for `ColumnCollectionBuilder.addSource()`.
19
19
  */
20
- export function collectCtxColumnSnapshotProviders<A, U, S>(
21
- ctx: RenderCtxBase<A, U, S>,
22
- ): ColumnSnapshotProvider[] {
20
+ export function collectCtxColumnSnapshotProviders(ctx: RenderCtxBase): ColumnSnapshotProvider[] {
23
21
  const providers: ColumnSnapshotProvider[] = [];
24
22
 
25
23
  // ResultPool — all upstream columns
@@ -214,8 +214,8 @@ describe("PFrameForGraph", () => {
214
214
  valueType: "String",
215
215
  annotations: { [Annotation.IsLinkerColumn]: "true" },
216
216
  axesSpec: [
217
- { type: "String", name: "axis1" },
218
217
  { type: "String", name: "axis3" },
218
+ { type: "String", name: "axis1" },
219
219
  ],
220
220
  };
221
221
 
@@ -240,8 +240,8 @@ describe("PFrameForGraph", () => {
240
240
  valueType: "String",
241
241
  annotations: { [Annotation.IsLinkerColumn]: "true" },
242
242
  axesSpec: [
243
- { type: "String", name: "axis1" },
244
243
  { type: "String", name: "axis2" },
244
+ { type: "String", name: "axis1" },
245
245
  ],
246
246
  };
247
247
  const linkerColumn23: PColumnSpec = {
@@ -250,8 +250,8 @@ describe("PFrameForGraph", () => {
250
250
  valueType: "String",
251
251
  annotations: { [Annotation.IsLinkerColumn]: "true" },
252
252
  axesSpec: [
253
- { type: "String", name: "axis2" },
254
253
  { type: "String", name: "axis3" },
254
+ { type: "String", name: "axis2" },
255
255
  ],
256
256
  };
257
257
  const linkerColumn34: PColumnSpec = {
@@ -260,8 +260,8 @@ describe("PFrameForGraph", () => {
260
260
  valueType: "String",
261
261
  annotations: { [Annotation.IsLinkerColumn]: "true" },
262
262
  axesSpec: [
263
- { type: "String", name: "axis3" },
264
263
  { type: "String", name: "axis4" },
264
+ { type: "String", name: "axis3" },
265
265
  ],
266
266
  };
267
267
 
@@ -33,8 +33,8 @@ export function createPTableDefV2(params: {
33
33
  secondaryColumns.push(...params.labelColumns);
34
34
 
35
35
  return createPTableDefV3({
36
- primaryColumns: coreColumns,
37
- secondaryGroups: secondaryColumns.map((c) => [c]),
36
+ primary: coreColumns.map((column) => ({ column })),
37
+ secondary: secondaryColumns.map((column) => ({ entries: [{ column }] })),
38
38
  primaryJoinType: params.coreJoinType,
39
39
  filters: params.filters,
40
40
  sorting: params.sorting,
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ AxisQualification,
2
3
  PColumn,
3
4
  PTableColumnId,
4
5
  PTableSorting,
@@ -7,6 +8,7 @@ import type {
7
8
  SpecQuery,
8
9
  SpecQueryExpression,
9
10
  SpecQueryJoinEntry,
11
+ PObjectId,
10
12
  } from "@milaboratories/pl-model-common";
11
13
  import { isBooleanExpression } from "@milaboratories/pl-model-common";
12
14
  import type { PColumnDataUniversal } from "../../../render";
@@ -15,31 +17,51 @@ import type { PlDataTableFilters } from "../typesV5";
15
17
  import { distillFilterSpec, filterSpecToSpecQueryExpr } from "../../../filters";
16
18
  import type { Nil } from "@milaboratories/helpers";
17
19
 
20
+ /** Primary side — base row grid. */
21
+ export type PrimaryEntry<Data> = {
22
+ column: PColumn<Data>;
23
+ };
24
+
25
+ /** Secondary side leaf — the hit column, a linker step, or a label column. */
26
+ export type SecondaryEntry<Data> = {
27
+ column: PColumn<Data>;
28
+ /** For hit: `forHit`. For linker step k: `path[k].qualifications`. For label/direct: omit. */
29
+ qualifications?: AxisQualification[];
30
+ };
31
+
32
+ /** Secondary group — one join subtree outer-joined onto primary. */
33
+ export type SecondaryGroup<Data> = {
34
+ entries: SecondaryEntry<Data>[];
35
+ /** Per-variant qualifications applied to the cloned primary anchors on this group's side.
36
+ * Keyed by `PrimaryEntry.column.id`. Omit → base primary used unqualified (labels, non-variant columns). */
37
+ primaryQualifications?: Record<PObjectId, AxisQualification[]>;
38
+ };
39
+
18
40
  export function createPTableDefV3<Data = PColumnDataUniversal>(params: {
19
41
  primaryJoinType: "inner" | "full";
20
- primaryColumns: PColumn<Data>[];
21
- secondaryGroups: PColumn<Data>[][];
42
+ primary: PrimaryEntry<Data>[];
43
+ secondary: SecondaryGroup<Data>[];
22
44
  filters?: Nil | PlDataTableFilters;
23
45
  sorting?: Nil | PTableSorting[];
24
46
  }): PTableDefV2<PColumn<Data>> {
25
- // Build SpecQuery directly from columns
26
- const coreJoinQuery: SpecQuery<PColumn<Data>> = {
47
+ let query: SpecQuery<PColumn<Data>> = {
27
48
  type: params.primaryJoinType === "inner" ? "innerJoin" : "fullJoin",
28
- entries: params.primaryColumns.map((c) => toJoinEntry({ type: "column", column: c })),
49
+ entries: params.primary.map((a) => toLeaf(a.column, [])),
29
50
  };
30
51
 
31
- let query: SpecQuery<PColumn<Data>> = {
32
- type: "outerJoin",
33
- primary: toJoinEntry(coreJoinQuery),
34
- secondary: params.secondaryGroups.map((group) =>
35
- toJoinEntry({
36
- type: "innerJoin" as const,
37
- entries: group.map((c) => toJoinEntry({ type: "column" as const, column: c })),
38
- }),
39
- ),
40
- };
52
+ for (const group of params.secondary) {
53
+ query = {
54
+ type: "outerJoin",
55
+ primary: {
56
+ entry: query,
57
+ qualifications: params.primary.flatMap((p) => {
58
+ return group.primaryQualifications?.[p.column.id] ?? [];
59
+ }),
60
+ },
61
+ secondary: group.entries.map((e) => toLeaf(e.column, e.qualifications ?? [])),
62
+ };
63
+ }
41
64
 
42
- // Apply filters
43
65
  if (!isNil(params.filters)) {
44
66
  const nonEmpty = distillFilterSpec(params.filters);
45
67
 
@@ -58,7 +80,6 @@ export function createPTableDefV3<Data = PColumnDataUniversal>(params: {
58
80
  }
59
81
  }
60
82
 
61
- // Apply sorting
62
83
  if (!isNil(params.sorting) && params.sorting.length > 0) {
63
84
  query = {
64
85
  type: "sort",
@@ -74,16 +95,18 @@ export function createPTableDefV3<Data = PColumnDataUniversal>(params: {
74
95
  return { query };
75
96
  }
76
97
 
77
- /** Convert a PTableColumnId to a SpecQueryExpression reference. */
78
98
  function columnIdToExpr(col: PTableColumnId): SpecQueryExpression {
79
99
  return col.type === "axis"
80
100
  ? { type: "axisRef", value: col.id as SingleAxisSelector }
81
101
  : { type: "columnRef", value: col.id };
82
102
  }
83
103
 
84
- function toJoinEntry<C>(input: SpecQuery<C>): SpecQueryJoinEntry<C> {
104
+ function toLeaf<Data>(
105
+ col: PColumn<Data>,
106
+ qs: AxisQualification[],
107
+ ): SpecQueryJoinEntry<PColumn<Data>> {
85
108
  return {
86
- entry: input,
87
- qualifications: [],
109
+ entry: { type: "column", column: col },
110
+ qualifications: qs,
88
111
  };
89
112
  }