@milaboratories/pl-model-common 1.31.2 → 1.32.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 (42) hide show
  1. package/dist/drivers/index.d.ts +4 -4
  2. package/dist/drivers/pframe/index.d.ts +4 -4
  3. package/dist/drivers/pframe/query/index.d.ts +2 -2
  4. package/dist/drivers/pframe/query/query_common.d.ts +39 -1
  5. package/dist/drivers/pframe/query/query_common.d.ts.map +1 -1
  6. package/dist/drivers/pframe/query/query_data.d.ts +18 -3
  7. package/dist/drivers/pframe/query/query_data.d.ts.map +1 -1
  8. package/dist/drivers/pframe/query/query_spec.d.ts +14 -3
  9. package/dist/drivers/pframe/query/query_spec.d.ts.map +1 -1
  10. package/dist/drivers/pframe/query/utils.cjs +27 -0
  11. package/dist/drivers/pframe/query/utils.cjs.map +1 -1
  12. package/dist/drivers/pframe/query/utils.d.ts.map +1 -1
  13. package/dist/drivers/pframe/query/utils.js +27 -0
  14. package/dist/drivers/pframe/query/utils.js.map +1 -1
  15. package/dist/drivers/pframe/spec/anchored.cjs.map +1 -1
  16. package/dist/drivers/pframe/spec/anchored.d.ts +3 -3
  17. package/dist/drivers/pframe/spec/anchored.d.ts.map +1 -1
  18. package/dist/drivers/pframe/spec/anchored.js.map +1 -1
  19. package/dist/drivers/pframe/spec/index.d.ts +1 -1
  20. package/dist/drivers/pframe/spec/spec.cjs +8 -3
  21. package/dist/drivers/pframe/spec/spec.cjs.map +1 -1
  22. package/dist/drivers/pframe/spec/spec.d.ts +15 -2
  23. package/dist/drivers/pframe/spec/spec.d.ts.map +1 -1
  24. package/dist/drivers/pframe/spec/spec.js +8 -3
  25. package/dist/drivers/pframe/spec/spec.js.map +1 -1
  26. package/dist/drivers/pframe/spec_driver.d.ts +37 -2
  27. package/dist/drivers/pframe/spec_driver.d.ts.map +1 -1
  28. package/dist/index.d.ts +5 -5
  29. package/dist/services/service_types.cjs.map +1 -1
  30. package/dist/services/service_types.d.ts +7 -7
  31. package/dist/services/service_types.d.ts.map +1 -1
  32. package/dist/services/service_types.js.map +1 -1
  33. package/package.json +6 -6
  34. package/src/drivers/pframe/query/query_common.ts +39 -0
  35. package/src/drivers/pframe/query/query_data.ts +20 -0
  36. package/src/drivers/pframe/query/query_spec.ts +17 -0
  37. package/src/drivers/pframe/query/utils.test.ts +37 -0
  38. package/src/drivers/pframe/query/utils.ts +25 -0
  39. package/src/drivers/pframe/spec/anchored.ts +8 -3
  40. package/src/drivers/pframe/spec/spec.ts +19 -6
  41. package/src/drivers/pframe/spec_driver.ts +42 -1
  42. package/src/services/service_types.ts +1 -6
@@ -74,6 +74,20 @@ describe("traverseQuerySpec", () => {
74
74
  });
75
75
  });
76
76
 
77
+ it("transforms columns inside linkerJoin", () => {
78
+ const q: Q = {
79
+ type: "linkerJoin",
80
+ linker: { column: "l" },
81
+ secondary: [entry(col("a")), entry(col("b"))],
82
+ };
83
+ const result = traverseQuerySpec(q, { column: (c) => c.toUpperCase() });
84
+ expect(result).toEqual({
85
+ type: "linkerJoin",
86
+ linker: { column: "L" },
87
+ secondary: [entry({ type: "column", column: "A" }), entry({ type: "column", column: "B" })],
88
+ });
89
+ });
90
+
77
91
  it("transforms columns inside filter", () => {
78
92
  const q: Q = {
79
93
  type: "filter",
@@ -195,6 +209,15 @@ describe("collectSpecQueryColumns", () => {
195
209
  } as unknown as Q;
196
210
  expect(collectSpecQueryColumns(q)).toEqual([]);
197
211
  });
212
+
213
+ it("collects linker and secondary columns from linkerJoin", () => {
214
+ const q: Q = {
215
+ type: "linkerJoin",
216
+ linker: { column: "l" },
217
+ secondary: [entry(col("a")), entry(col("b"))],
218
+ };
219
+ expect(collectSpecQueryColumns(q)).toEqual(["l", "a", "b"]);
220
+ });
198
221
  });
199
222
 
200
223
  describe("sortSpecQuery", () => {
@@ -221,6 +244,20 @@ describe("sortSpecQuery", () => {
221
244
  });
222
245
  });
223
246
 
247
+ it("sorts linkerJoin secondary entries (linker column unchanged)", () => {
248
+ const q: SpecQuery = {
249
+ type: "linkerJoin",
250
+ linker: { column: pid("l") },
251
+ secondary: [pentry(pcol("c")), pentry(pcol("a")), pentry(pcol("b"))],
252
+ };
253
+ const result = sortSpecQuery(q);
254
+ expect(result).toEqual({
255
+ type: "linkerJoin",
256
+ linker: { column: "l" },
257
+ secondary: [pentry(pcol("a")), pentry(pcol("b")), pentry(pcol("c"))],
258
+ });
259
+ });
260
+
224
261
  it("sorts sparseToDenseColumn axesIndices", () => {
225
262
  const q: SpecQuery = {
226
263
  type: "sparseToDenseColumn",
@@ -78,6 +78,13 @@ export function traverseQuerySpec<C1, C2>(
78
78
  secondary: query.secondary.map(traverseEntry),
79
79
  };
80
80
  break;
81
+ case "linkerJoin":
82
+ result = {
83
+ ...query,
84
+ linker: { ...query.linker, column: visitor.column(query.linker.column) },
85
+ secondary: query.secondary.map(traverseEntry),
86
+ };
87
+ break;
81
88
  case "filter":
82
89
  case "sort":
83
90
  case "sliceAxes":
@@ -126,6 +133,10 @@ export function sortSpecQuery(query: SpecQuery): SpecQuery {
126
133
  const sorted = [...node.secondary].sort(cmpQueryJoinEntrySpec);
127
134
  return { ...node, secondary: sorted };
128
135
  }
136
+ case "linkerJoin": {
137
+ const sorted = [...node.secondary].sort(cmpQueryJoinEntrySpec);
138
+ return { ...node, secondary: sorted };
139
+ }
129
140
  case "sliceAxes":
130
141
  return {
131
142
  ...node,
@@ -198,6 +209,20 @@ function cmpQuerySpec(lhs: SpecQuery, rhs: SpecQuery): number {
198
209
  }
199
210
  return 0;
200
211
  }
212
+ case "linkerJoin": {
213
+ const rhsLinker = rhs as typeof lhs;
214
+ if (lhs.linker.column !== rhsLinker.linker.column) {
215
+ return lhs.linker.column < rhsLinker.linker.column ? -1 : 1;
216
+ }
217
+ if (lhs.secondary.length !== rhsLinker.secondary.length) {
218
+ return lhs.secondary.length - rhsLinker.secondary.length;
219
+ }
220
+ for (let i = 0; i < lhs.secondary.length; i++) {
221
+ const cmp = cmpQueryJoinEntrySpec(lhs.secondary[i], rhsLinker.secondary[i]);
222
+ if (cmp !== 0) return cmp;
223
+ }
224
+ return 0;
225
+ }
201
226
  case "sliceAxes":
202
227
  return cmpQuerySpec(lhs.input, (rhs as typeof lhs).input);
203
228
  case "sort":
@@ -50,7 +50,12 @@ export class AnchoredIdDeriver {
50
50
  * Creates a new anchor context from a set of anchor column specifications
51
51
  * @param anchors Record of anchor column specifications indexed by anchor ID
52
52
  */
53
- constructor(public readonly anchors: Record<string, PColumnSpec>) {
53
+ constructor(
54
+ public readonly anchors: Record<
55
+ string,
56
+ Pick<PColumnSpec, "axesSpec" | "domain" | "contextDomain">
57
+ >,
58
+ ) {
54
59
  const anchorEntries = Object.entries(anchors);
55
60
  anchorEntries.sort((a, b) => a[0].localeCompare(b[0]));
56
61
  for (const [anchorId, spec] of anchorEntries) {
@@ -241,7 +246,7 @@ export type ResolveAnchorsOptions = {
241
246
  * @returns A non-anchored column matcher with all references resolved to actual values
242
247
  */
243
248
  export function resolveAnchors(
244
- anchors: Record<string, PColumnSpec>,
249
+ anchors: Record<string, Pick<PColumnSpec, "axesSpec" | "domain" | "contextDomain">>,
245
250
  matcher: AnchoredPColumnSelector,
246
251
  options?: ResolveAnchorsOptions,
247
252
  ): PColumnSelector {
@@ -321,7 +326,7 @@ export function resolveAnchors(
321
326
  * Resolves an anchored axis reference to a concrete AxisId
322
327
  */
323
328
  function resolveAxisReference(
324
- anchors: Record<string, PColumnSpec>,
329
+ anchors: Record<string, Pick<PColumnSpec, "axesSpec">>,
325
330
  axisRef: AAxisSelector,
326
331
  ): LegacyAxisSelector {
327
332
  if (!isAnchorAxisRef(axisRef)) return axisRef;
@@ -145,6 +145,7 @@ export const Annotation = {
145
145
  IsScore: "pl7.app/isScore",
146
146
  IsSubset: "pl7.app/isSubset",
147
147
  Label: "pl7.app/label",
148
+ LinkLabel: "pl7.app/linkLabel",
148
149
  Max: "pl7.app/max",
149
150
  Min: "pl7.app/min",
150
151
  MultipliesBy: "pl7.app/multipliesBy",
@@ -204,10 +205,24 @@ export type Annotation = Metadata &
204
205
  [Annotation.Table.FontFamily]: string;
205
206
  [Annotation.Table.OrderPriority]: StringifiedJson<number>;
206
207
  [Annotation.Table.Visibility]: "hidden" | "optional" | (string & {});
207
- [Annotation.Trace]: StringifiedJson<Record<string, unknown>>;
208
+ [Annotation.Trace]: StringifiedJson<Trace>;
208
209
  [Annotation.VDJ.IsAssemblingFeature]: StringifiedJson<boolean>;
209
210
  }>;
210
211
 
212
+ /// One step in a column's derivation lineage. Only `type` and `label` are part
213
+ /// of the typed public surface; writers may add fields (e.g. `id`, `importance`)
214
+ /// passed through opaquely as `unknown`, accessed via explicit narrowing.
215
+ const TraceEntrySchema = z
216
+ .object({
217
+ type: z.string(),
218
+ label: z.string(),
219
+ })
220
+ .catchall(z.unknown());
221
+
222
+ export type TraceEntry = z.infer<typeof TraceEntrySchema>;
223
+
224
+ export type Trace = TraceEntry[];
225
+
211
226
  // export const AxisSpec = z.object({
212
227
  // type: z.nativeEnum(ValueType),
213
228
  // name: z.string(),
@@ -256,7 +271,7 @@ export const AnnotationJson: AnnotationJson = {
256
271
  [Annotation.Sequence.Annotation.Mapping]: z.record(z.string(), z.string()),
257
272
  [Annotation.Sequence.IsAnnotation]: z.boolean(),
258
273
  [Annotation.Table.OrderPriority]: z.number(),
259
- [Annotation.Trace]: z.record(z.string(), z.unknown()),
274
+ [Annotation.Trace]: z.array(TraceEntrySchema),
260
275
  [Annotation.VDJ.IsAssemblingFeature]: z.boolean(),
261
276
  };
262
277
 
@@ -527,11 +542,9 @@ export function getNormalizedAxesList(axes: AxisSpec[]): AxisSpecNormalized[] {
527
542
 
528
543
  /** Create list of regular axisSpec from normalized (parents are indexes, inside of current axes list) */
529
544
  export function getDenormalizedAxesList(axesSpec: AxisSpecNormalized[]): AxisSpec[] {
530
- const idsList = axesSpec.map((axisSpec) => canonicalizeJson(getAxisId(axisSpec)));
545
+ const idsList = axesSpec.map(canonicalizeAxisId);
531
546
  return axesSpec.map((axisSpec) => {
532
- const parentsIds = axisSpec.parentAxesSpec.map((axisSpec) =>
533
- canonicalizeJson(getAxisId(axisSpec)),
534
- );
547
+ const parentsIds = axisSpec.parentAxesSpec.map(canonicalizeAxisId);
535
548
  const parentIdxs = parentsIds.map((id) => idsList.indexOf(id));
536
549
  const { parentAxesSpec: _, ...copiedRest } = axisSpec;
537
550
  if (parentIdxs.length) {
@@ -1,5 +1,6 @@
1
1
  import type { Branded } from "@milaboratories/helpers";
2
2
  import type { PoolEntry } from "../../pool_entry";
3
+ import type { PObjectId } from "../../pool";
3
4
  import type {
4
5
  AxisSpec,
5
6
  AxesSpec,
@@ -111,8 +112,48 @@ export interface DiscoverColumnsLinkerStep {
111
112
  qualifications: AxisQualification[];
112
113
  }
113
114
 
115
+ /**
116
+ * Filter step: intersects the current subquery with a filter column on shared
117
+ * axes. Filter columns carry `pl7.app/isSubset: "true"` with axes ⊆ dataset
118
+ * axes, so the inner-join narrows the key space to rows where the filter is
119
+ * present.
120
+ */
121
+ export interface DiscoverColumnsFilterStep {
122
+ type: "filter";
123
+ /** The filter column applied in this step */
124
+ filter: PColumnIdAndSpec;
125
+ }
126
+
114
127
  /** A step traversed during path-based column discovery. Discriminated by `type`. */
115
- export type DiscoverColumnsStepInfo = DiscoverColumnsLinkerStep;
128
+ export type DiscoverColumnsStepInfo = DiscoverColumnsLinkerStep | DiscoverColumnsFilterStep;
129
+
130
+ /**
131
+ * Input to {@link PFrameWasmV4.buildQuery}: a terminal column plus an ordered
132
+ * path of wrapping steps (linker hops, filter joins). Produces a
133
+ * {@link SpecQueryJoinEntry} ready to be plugged into an
134
+ * `innerJoin`/`fullJoin`/`outerJoin` entry list.
135
+ *
136
+ * Path ordering: `path[0]` is outermost (first applied), `path[N-1]` is
137
+ * closest to `column`. Omit or pass `[]` for a direct column with no
138
+ * wrapping.
139
+ *
140
+ * Columns are referenced by id — specs are resolved later at
141
+ * {@link PFrameWasmV4.evaluateQuery} against the registered specs of the
142
+ * PFrame, so the caller cannot disagree with the frame about spec content.
143
+ *
144
+ * Qualifications annotate the resulting outermost entry; they do not
145
+ * propagate into the inner query.
146
+ */
147
+ export type BuildQueryInput = {
148
+ /** Shape version marker — bumped only on breaking structural changes. */
149
+ readonly version: "v1";
150
+ /** Terminal column id — the column actually returning data. */
151
+ readonly column: PObjectId;
152
+ /** Ordered path from source integration to `column`. Outermost first. */
153
+ readonly path?: DiscoverColumnsStepInfo[];
154
+ /** Axis qualifications attached to the resulting join entry. */
155
+ readonly qualifications?: AxisQualification[];
156
+ };
116
157
 
117
158
  /** Qualifications info for a discover columns response mapping variant */
118
159
  export interface DiscoverColumnsResponseQualifications {
@@ -1,3 +1,4 @@
1
+ import { UnionToIntersection } from "@milaboratories/helpers";
1
2
  import type { Branded } from "../branding";
2
3
  import { ServiceAlreadyRegisteredError, ServiceInvalidIdError } from "../errors";
3
4
  import type { Services } from "./service_declarations";
@@ -101,12 +102,6 @@ export type ServiceRequireFlags = {
101
102
  [K in keyof SMap as `requires${K & string}`]?: boolean;
102
103
  };
103
104
 
104
- type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (
105
- k: infer I,
106
- ) => void
107
- ? I
108
- : never;
109
-
110
105
  export type RequireServices<T extends ServiceName> = UnionToIntersection<
111
106
  T extends Branded<infer N extends string, infer S extends ServiceTypesLike>
112
107
  ? Record<N, InferServiceModel<S>>