@milaboratories/pl-middle-layer 1.65.8 → 1.66.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 (52) hide show
  1. package/dist/js_render/column_registry.cjs +56 -0
  2. package/dist/js_render/column_registry.cjs.map +1 -0
  3. package/dist/js_render/column_registry.js +55 -0
  4. package/dist/js_render/column_registry.js.map +1 -0
  5. package/dist/js_render/computable_context.cjs +80 -12
  6. package/dist/js_render/computable_context.cjs.map +1 -1
  7. package/dist/js_render/computable_context.js +81 -13
  8. package/dist/js_render/computable_context.js.map +1 -1
  9. package/dist/js_render/service_injectors.cjs +43 -2
  10. package/dist/js_render/service_injectors.cjs.map +1 -1
  11. package/dist/js_render/service_injectors.js +43 -2
  12. package/dist/js_render/service_injectors.js.map +1 -1
  13. package/dist/js_render/spec_override_collapse.cjs +108 -0
  14. package/dist/js_render/spec_override_collapse.cjs.map +1 -0
  15. package/dist/js_render/spec_override_collapse.js +108 -0
  16. package/dist/js_render/spec_override_collapse.js.map +1 -0
  17. package/dist/middle_layer/block_ctx.cjs +2 -1
  18. package/dist/middle_layer/block_ctx.cjs.map +1 -1
  19. package/dist/middle_layer/block_ctx.js +2 -1
  20. package/dist/middle_layer/block_ctx.js.map +1 -1
  21. package/dist/mutator/template/template_cache.cjs.map +1 -1
  22. package/dist/mutator/template/template_cache.d.ts.map +1 -1
  23. package/dist/mutator/template/template_cache.js.map +1 -1
  24. package/dist/pool/data.cjs +2 -9
  25. package/dist/pool/data.cjs.map +1 -1
  26. package/dist/pool/data.d.ts.map +1 -1
  27. package/dist/pool/data.js +3 -10
  28. package/dist/pool/data.js.map +1 -1
  29. package/dist/pool/result_pool.cjs +0 -20
  30. package/dist/pool/result_pool.cjs.map +1 -1
  31. package/dist/pool/result_pool.js +1 -21
  32. package/dist/pool/result_pool.js.map +1 -1
  33. package/dist/pool/upstream_block_ctx.cjs +64 -0
  34. package/dist/pool/upstream_block_ctx.cjs.map +1 -0
  35. package/dist/pool/upstream_block_ctx.js +64 -0
  36. package/dist/pool/upstream_block_ctx.js.map +1 -0
  37. package/dist/service_factories.cjs +3 -1
  38. package/dist/service_factories.cjs.map +1 -1
  39. package/dist/service_factories.js +3 -1
  40. package/dist/service_factories.js.map +1 -1
  41. package/package.json +16 -15
  42. package/src/js_render/column_registry.ts +95 -0
  43. package/src/js_render/computable_context.ts +109 -26
  44. package/src/js_render/service_injectors.ts +106 -8
  45. package/src/js_render/spec_override_collapse.ts +211 -0
  46. package/src/middle_layer/block_ctx.ts +6 -1
  47. package/src/mutator/template/template_cache.test.ts +0 -14
  48. package/src/mutator/template/template_cache.ts +0 -14
  49. package/src/pool/data.ts +4 -3
  50. package/src/pool/result_pool.ts +1 -24
  51. package/src/pool/upstream_block_ctx.ts +85 -0
  52. package/src/service_factories.ts +2 -0
@@ -0,0 +1,211 @@
1
+ import type { PlTreeNodeAccessor } from "@milaboratories/pl-tree";
2
+ import type { DataInfo, PColumn, PColumnValues } from "@platforma-sdk/model";
3
+ import { applySpecOverrides, matchAxis } from "@milaboratories/pl-model-common";
4
+ import type {
5
+ AxesSpec,
6
+ AxisPatches,
7
+ SpecOverrides,
8
+ SpecQuery,
9
+ } from "@milaboratories/pl-model-common";
10
+
11
+ type Leaf = PColumn<PlTreeNodeAccessor | PColumnValues | DataInfo<PlTreeNodeAccessor>>;
12
+ type Node = SpecQuery<Leaf>;
13
+
14
+ /**
15
+ * Collapse the client-side `specOverride` query node at the host boundary —
16
+ * pframe-engine never sees it. The override is pushed down through the
17
+ * subtree until it lands on `column`-leaf nodes, where it is folded into
18
+ * the leaf's spec via {@link applySpecOverrides}. Intermediate join nodes
19
+ * are left untouched — pframe-engine will recompute their result spec
20
+ * from the patched leaves.
21
+ *
22
+ * Designed to run as the `node` visitor in `mapPTableDefV2` (post-order),
23
+ * so by the time we see a `specOverride` its inner subtree is already
24
+ * mapped to {@link PColumn} leaves.
25
+ *
26
+ * Currently only `column` and `linkerJoin` are walked. Other node shapes
27
+ * under `specOverride` throw — pframe-engine support for them is still
28
+ * in progress. Extend the switch in {@link pushSpecOverrideDown} as more
29
+ * shapes become needed.
30
+ */
31
+ export function collapseSpecOverrideNode(node: Node): Node {
32
+ if (node.type !== "specOverride") return node;
33
+ return pushSpecOverrideDown(node.input, node.override);
34
+ }
35
+
36
+ function pushSpecOverrideDown(node: Node, overrides: SpecOverrides): Node {
37
+ switch (node.type) {
38
+ case "column":
39
+ return {
40
+ type: "column",
41
+ column: { ...node.column, spec: applySpecOverrides(node.column.spec, overrides) },
42
+ };
43
+
44
+ case "linkerJoin": {
45
+ // `axesSpec` patches are positional against the linkerJoin's OUTPUT axes,
46
+ // which equal the secondary side's axes (ColumnDiscoveredRecipe emits the
47
+ // hit on the many-side of the linker). Route axesSpec only into secondary;
48
+ // applying the same positional indices to the linker would silently
49
+ // mis-target the linker's own axes.
50
+ //
51
+ // Non-axesSpec patches (domain / contextDomain / annotations) are
52
+ // column-level and not positional — keep the existing both-sides walk.
53
+ const { axesSpec: _axesSpec, ...nonAxesOverrides } = overrides;
54
+ const hasNonAxes =
55
+ nonAxesOverrides.domain !== undefined ||
56
+ nonAxesOverrides.contextDomain !== undefined ||
57
+ nonAxesOverrides.annotations !== undefined;
58
+ return {
59
+ ...node,
60
+ linker: hasNonAxes ? pushSpecOverrideDown(node.linker, nonAxesOverrides) : node.linker,
61
+ secondary: node.secondary.map((e) => ({
62
+ ...e,
63
+ entry: pushSpecOverrideDown(e.entry, overrides),
64
+ })),
65
+ };
66
+ }
67
+
68
+ // case "outerJoin":
69
+ // return {
70
+ // ...node,
71
+ // primary: {
72
+ // ...node.primary,
73
+ // entry: pushSpecOverrideDown(node.primary.entry, overrides),
74
+ // },
75
+ // secondary: node.secondary.map((e) => ({
76
+ // ...e,
77
+ // entry: pushSpecOverrideDown(e.entry, overrides),
78
+ // })),
79
+ // };
80
+
81
+ // case "innerJoin":
82
+ // case "fullJoin":
83
+ // case "symmetricJoin":
84
+ // return {
85
+ // ...node,
86
+ // entries: node.entries.map((e) => ({
87
+ // ...e,
88
+ // entry: pushSpecOverrideDown(e.entry, overrides),
89
+ // })),
90
+ // };
91
+
92
+ case "sliceAxes": {
93
+ // axesSpec patches are positional against the slice OUTPUT axes (the
94
+ // recipe-layer spec the consumer sees), but the inner input still
95
+ // carries the full pre-slice axes list. Translate outer indices to
96
+ // inner indices before pushing the override down.
97
+ //
98
+ // Non-axesSpec overrides (domain / contextDomain / annotations) are
99
+ // column-level and pass through transparently.
100
+ if (overrides.axesSpec === undefined || Object.keys(overrides.axesSpec).length === 0) {
101
+ return { ...node, input: pushSpecOverrideDown(node.input, overrides) };
102
+ }
103
+
104
+ const innerAxes = effectiveAxesSpec(node.input);
105
+ if (innerAxes === undefined) {
106
+ throw new Error(
107
+ `specOverride over sliceAxes: cannot translate axesSpec — ` +
108
+ `inner shape unknown for input type "${node.input.type}"`,
109
+ );
110
+ }
111
+
112
+ const filteredInnerIdxs = new Set<number>();
113
+ for (const filter of node.axisFilters) {
114
+ const idx = innerAxes.findIndex((a) => matchAxis(filter.axisSelector, a));
115
+ if (idx < 0) {
116
+ throw new Error(
117
+ `specOverride over sliceAxes: filter selector ` +
118
+ `${JSON.stringify(filter.axisSelector)} not found in inner axes`,
119
+ );
120
+ }
121
+ filteredInnerIdxs.add(idx);
122
+ }
123
+
124
+ // outer index j (post-slice) ↔ inner index = j-th surviving position.
125
+ const outerToInner: number[] = [];
126
+ for (let i = 0; i < innerAxes.length; i++) {
127
+ if (!filteredInnerIdxs.has(i)) outerToInner.push(i);
128
+ }
129
+
130
+ const translated: AxisPatches = {};
131
+ for (const [outerKey, patch] of Object.entries(overrides.axesSpec)) {
132
+ const outerIdx = Number(outerKey);
133
+ const innerIdx = outerToInner[outerIdx];
134
+ if (innerIdx === undefined) {
135
+ throw new Error(
136
+ `specOverride over sliceAxes: outer axesSpec index ${outerIdx} ` +
137
+ `out of range (slice output has ${outerToInner.length} axes)`,
138
+ );
139
+ }
140
+ translated[innerIdx] = patch;
141
+ }
142
+
143
+ const translatedOverrides: SpecOverrides = { ...overrides, axesSpec: translated };
144
+ return { ...node, input: pushSpecOverrideDown(node.input, translatedOverrides) };
145
+ }
146
+
147
+ // // Transparent transforms — passthrough.
148
+ // case "sort":
149
+ // case "filter":
150
+ // case "transformColumns":
151
+ // return { ...node, input: pushSpecOverrideDown(node.input, overrides) };
152
+
153
+ // // Synthetic leaves — patch the spec carrier they expose, or
154
+ // // throw if the node has no natural place to absorb overrides.
155
+ // case "inlineColumn":
156
+ // return {
157
+ // ...node,
158
+ // column: { ...node.column, spec: applySpecOverrides(node.column.spec, overrides) },
159
+ // };
160
+
161
+ // case "sparseToDenseColumn":
162
+ // // Same idea: patch the new dense column's spec carrier.
163
+ // return {
164
+ // ...node,
165
+ // newSpec: applySpecOverrides(node.newSpec, overrides),
166
+ // };
167
+
168
+ default:
169
+ throw new Error(
170
+ `specOverride: inner node "${node.type}" is not supported yet — ` +
171
+ "only column / linkerJoin can carry an override " +
172
+ "(pframe-engine support for the rest is in progress)",
173
+ );
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Effective post-traversal {@link AxesSpec} of a {@link Node} — used to
179
+ * translate positional axesSpec patches across structural nodes whose
180
+ * indexing differs from their input (currently only `sliceAxes`).
181
+ *
182
+ * Returns `undefined` when the node shape is not introspectable here.
183
+ * Callers must then refuse axesSpec translation and surface a clear error,
184
+ * rather than silently mis-targeting indices.
185
+ */
186
+ function effectiveAxesSpec(node: Node): AxesSpec | undefined {
187
+ switch (node.type) {
188
+ case "column":
189
+ return node.column.spec.axesSpec;
190
+
191
+ case "linkerJoin":
192
+ // Output axes equal the secondary side's axes (linker side drops out).
193
+ // All secondary entries share the same axis identity post-join — read
194
+ // the first one.
195
+ return node.secondary.length === 0 ? undefined : effectiveAxesSpec(node.secondary[0].entry);
196
+
197
+ case "sliceAxes": {
198
+ const inner = effectiveAxesSpec(node.input);
199
+ if (inner === undefined) return undefined;
200
+ const filtered = new Set<number>();
201
+ for (const f of node.axisFilters) {
202
+ const i = inner.findIndex((a) => matchAxis(f.axisSelector, a));
203
+ if (i >= 0) filtered.add(i);
204
+ }
205
+ return inner.filter((_, i) => !filtered.has(i));
206
+ }
207
+
208
+ default:
209
+ return undefined;
210
+ }
211
+ }
@@ -22,9 +22,13 @@ export type BlockContextFull = BlockContextArgsOnly & {
22
22
  readonly prod: (cCtx: ComputableCtx) => PlTreeEntry | undefined;
23
23
  readonly staging: (cCtx: ComputableCtx) => PlTreeEntry | undefined;
24
24
  readonly getResultsPool: (cCtx: ComputableCtx) => ResultPool;
25
+ readonly projectEntry: PlTreeEntry;
25
26
  };
26
27
 
27
- export type BlockContextAny = Optional<BlockContextFull, "prod" | "staging" | "getResultsPool">;
28
+ export type BlockContextAny = Optional<
29
+ BlockContextFull,
30
+ "prod" | "staging" | "getResultsPool" | "projectEntry"
31
+ >;
28
32
 
29
33
  export function constructBlockContextArgsOnly(
30
34
  projectEntry: PlTreeEntry,
@@ -159,5 +163,6 @@ export function constructBlockContext(
159
163
  return result;
160
164
  },
161
165
  getResultsPool: (cCtx: ComputableCtx) => ResultPool.create(cCtx, projectEntry, blockId),
166
+ projectEntry,
162
167
  };
163
168
  }
@@ -39,8 +39,6 @@ function createTestCacheInTx(pl: Parameters<Parameters<typeof TestHelpers.withTe
39
39
  });
40
40
  }
41
41
 
42
- // ─── flattenTemplateTree ─────────────────────────────────────────────────────
43
-
44
42
  describe("flattenTemplateTree", () => {
45
43
  test("produces nodes in topological order for V2 template", () => {
46
44
  const data = parseTemplate(ExplicitTemplateEnterNumbers);
@@ -77,8 +75,6 @@ describe("flattenTemplateTree", () => {
77
75
  });
78
76
  });
79
77
 
80
- // ─── getOrCreateTemplateCache / dropTemplateCache ────────────────────────────
81
-
82
78
  describe("getOrCreateTemplateCache", () => {
83
79
  test("creates cache on first call and reuses on second", async () => {
84
80
  await TestHelpers.withTempRoot(async (pl) => {
@@ -100,8 +96,6 @@ describe("dropTemplateCache", () => {
100
96
  });
101
97
  });
102
98
 
103
- // ─── loadTemplateCached ──────────────────────────────────────────────────────
104
-
105
99
  describe("loadTemplateCached", () => {
106
100
  test("cache miss then cache hit returns same SignedResourceId", async () => {
107
101
  await TestHelpers.withTempRoot(async (pl) => {
@@ -184,8 +178,6 @@ describe("loadTemplateCached", () => {
184
178
  }, 15000);
185
179
  });
186
180
 
187
- // ─── cacheBlockPackTemplate ──────────────────────────────────────────────────
188
-
189
181
  describe("cacheBlockPackTemplate", () => {
190
182
  test("replaces prepared template with cached reference", async () => {
191
183
  await TestHelpers.withTempRoot(async (pl) => {
@@ -235,8 +227,6 @@ describe("cacheBlockPackTemplate", () => {
235
227
  }, 15000);
236
228
  });
237
229
 
238
- // ─── Equivalence: cached vs legacy produce identical resources ───────────────
239
-
240
230
  describe("template cache produces equivalent resources", () => {
241
231
  test("cached and legacy templates deduplicate to same original (V2)", async () => {
242
232
  await TestHelpers.withTempRoot(async (pl) => {
@@ -272,8 +262,6 @@ describe("template cache produces equivalent resources", () => {
272
262
  }, 30000);
273
263
  });
274
264
 
275
- // ─── Shared library dedup ────────────────────────────────────────────────────
276
-
277
265
  describe("shared library dedup", () => {
278
266
  test("two different templates sharing a library reuse the same cache entry", async () => {
279
267
  await TestHelpers.withTempRoot(async (pl) => {
@@ -303,8 +291,6 @@ describe("shared library dedup", () => {
303
291
  }, 15000);
304
292
  });
305
293
 
306
- // ─── GC ──────────────────────────────────────────────────────────────────────
307
-
308
294
  describe("GC", () => {
309
295
  test("evicts entries when cache exceeds max entries", async () => {
310
296
  await TestHelpers.withTempRoot(async (pl) => {
@@ -48,8 +48,6 @@ export const ACCESS_COUNT_KEY = "_accessCount";
48
48
  /** @internal exported for testing */
49
49
  export const ACCESS_KEY_PREFIX = "access_";
50
50
 
51
- // ─── Stats ───────────────────────────────────────────────────────────────────
52
-
53
51
  export type TemplateCacheStat = {
54
52
  totalMs: number;
55
53
  flattenMs: number;
@@ -82,8 +80,6 @@ function initialStat(): TemplateCacheStat {
82
80
  };
83
81
  }
84
82
 
85
- // ─── Tree node abstraction ───────────────────────────────────────────────────
86
-
87
83
  interface CacheableNode {
88
84
  /** SHA-256 content hash (includes all descendant content) */
89
85
  hash: string;
@@ -94,8 +90,6 @@ interface CacheableNode {
94
90
  childHashes: string[];
95
91
  }
96
92
 
97
- // ─── Hash computation helpers ────────────────────────────────────────────────
98
-
99
93
  function getSourceCode(name: string, sources: Record<string, string>, sourceHash: string): string {
100
94
  return notEmpty(
101
95
  sources[sourceHash],
@@ -448,8 +442,6 @@ export function flattenTemplateTree(data: TemplateData | CompiledTemplateV3): Ca
448
442
  }
449
443
  }
450
444
 
451
- // ─── Cache operations ────────────────────────────────────────────────────────
452
-
453
445
  /** In-memory cache for the TemplateCache SignedResourceId per PlClient instance. */
454
446
  const cacheRidMap = new WeakMap<PlClient, SignedResourceId>();
455
447
 
@@ -502,8 +494,6 @@ export async function dropTemplateCache(pl: PlClient): Promise<void> {
502
494
  invalidateTemplateCacheId(pl);
503
495
  }
504
496
 
505
- // ─── GC ──────────────────────────────────────────────────────────────────────
506
-
507
497
  /**
508
498
  * Run count-based garbage collection on the template cache.
509
499
  * Evicts least-recently-used entries when the cache exceeds maxEntries.
@@ -549,8 +539,6 @@ export async function runGc(
549
539
  });
550
540
  }
551
541
 
552
- // ─── Batched materialization ─────────────────────────────────────────────────
553
-
554
542
  /** Create a batch of cache nodes in the current transaction. */
555
543
  function createBatchNodes(
556
544
  tx: PlTransaction,
@@ -788,8 +776,6 @@ export async function loadTemplateCached(
788
776
  }
789
777
  }
790
778
 
791
- // ─── Caller helper ───────────────────────────────────────────────────────────
792
-
793
779
  /**
794
780
  * Pre-materialize a block pack's template via cache.
795
781
  * Returns a copy of the spec with the template replaced by a cached reference.
package/src/pool/data.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import {
2
+ createGlobalPObjectId,
3
+ createLocalPObjectId,
2
4
  PFrameDriverError,
3
5
  type BinaryChunk,
4
6
  type ParquetChunk,
5
7
  type ParquetChunkMapping,
6
8
  type ParquetChunkMetadata,
7
9
  type PColumnValue,
8
- type PlRef,
9
10
  type PObjectId,
10
11
  type PObjectSpec,
11
12
  } from "@platforma-sdk/model";
@@ -331,9 +332,9 @@ export function deriveLegacyPObjectId(spec: PObjectSpec, data: PlTreeNodeAccesso
331
332
  }
332
333
 
333
334
  export function deriveGlobalPObjectId(blockId: string, exportName: string): PObjectId {
334
- return canonicalize({ __isRef: true, blockId, name: exportName } satisfies PlRef)! as PObjectId;
335
+ return createGlobalPObjectId(blockId, exportName);
335
336
  }
336
337
 
337
338
  export function deriveLocalPObjectId(resolvePath: string[], outputName: string): PObjectId {
338
- return canonicalize({ resolvePath, name: outputName })! as PObjectId;
339
+ return createLocalPObjectId(resolvePath, outputName);
339
340
  }
@@ -4,13 +4,12 @@ import type {
4
4
  Option,
5
5
  PObject,
6
6
  PObjectSpec,
7
- PSpecPredicate,
8
7
  PlRef,
9
8
  ResultCollection,
10
9
  ResultPoolEntry,
11
10
  ValueOrError,
12
11
  } from "@platforma-sdk/model";
13
- import { executePSpecPredicate, mapValueInVOE } from "@platforma-sdk/model";
12
+ import { mapValueInVOE } from "@platforma-sdk/model";
14
13
  import { notEmpty } from "@milaboratories/ts-helpers";
15
14
  import { outputRef } from "../model/args";
16
15
  import type { Block, ProjectStructure } from "../model/project_model";
@@ -239,28 +238,6 @@ export class ResultPool {
239
238
  return { entries, isComplete, instabilityMarker };
240
239
  }
241
240
 
242
- public calculateOptions(predicate: PSpecPredicate): ExtendedOption[] {
243
- const found: ExtendedOption[] = [];
244
- for (const block of this.blocks.values()) {
245
- const exportsChecked = new Set<string>();
246
- const addToFound = (ctx: RawPObjectCollection) => {
247
- for (const [exportName, result] of ctx.results) {
248
- if (exportsChecked.has(exportName) || result.spec === undefined) continue;
249
- exportsChecked.add(exportName);
250
- if (executePSpecPredicate(predicate, result.spec))
251
- found.push({
252
- label: block.info.label + " / " + exportName,
253
- ref: outputRef(block.info.id, exportName),
254
- spec: result.spec,
255
- });
256
- }
257
- };
258
- if (block.staging !== undefined) addToFound(block.staging);
259
- if (block.prod !== undefined) addToFound(block.prod);
260
- }
261
- return found;
262
- }
263
-
264
241
  public static create(ctx: ComputableCtx, prjEntry: PlTreeEntry, rootBlockId: string): ResultPool {
265
242
  const prj = ctx.accessor(prjEntry).node();
266
243
 
@@ -0,0 +1,85 @@
1
+ import type { ComputableCtx } from "@milaboratories/computable";
2
+ import type { PlTreeEntry, PlTreeNodeAccessor } from "@milaboratories/pl-tree";
3
+ import { notEmpty } from "@milaboratories/ts-helpers";
4
+ import type { UpstreamBlockCtx } from "@milaboratories/pl-model-common";
5
+ import type { ProjectStructure } from "../model/project_model";
6
+ import { ProjectStructureKey, projectFieldName } from "../model/project_model";
7
+ import { allBlocks, stagingGraph } from "../model/project_model_util";
8
+
9
+ export type { UpstreamBlockCtx } from "@milaboratories/pl-model-common";
10
+
11
+ /**
12
+ * Collect upstream-block ctx accessors for the given root block.
13
+ *
14
+ * For each upstream block (per the staging graph) we try to resolve its
15
+ * `prodUiCtx` and `stagingUiCtx` resources. Only the `.ok` outcomes are
16
+ * surfaced; failures and missing fields collapse to `undefined`.
17
+ *
18
+ * SDK-side providers compose enumerate/status/data operations on top of these
19
+ * accessors using the helpers in `column_providers/accessor_traversal`.
20
+ *
21
+ * Note: this function is the only place that reads the project tree to build
22
+ * the upstream-pool view — the legacy `ResultPool` class is not used.
23
+ */
24
+ export function collectUpstreamBlockCtx(
25
+ ctx: ComputableCtx,
26
+ prjEntry: PlTreeEntry,
27
+ rootBlockId: string,
28
+ ): UpstreamBlockCtx<PlTreeNodeAccessor>[] {
29
+ const prj = ctx.accessor(prjEntry).node();
30
+ const structure = notEmpty(prj.getKeyValueAsJson<ProjectStructure>(ProjectStructureKey));
31
+ const graph = stagingGraph(structure);
32
+ const targetBlocks = graph.traverseIds("upstream", rootBlockId);
33
+
34
+ const out: UpstreamBlockCtx<PlTreeNodeAccessor>[] = [];
35
+ for (const blockInfo of allBlocks(structure)) {
36
+ if (!targetBlocks.has(blockInfo.id)) continue;
37
+
38
+ const prod = resolveCtxAccessor(prj, blockInfo.id, "prod");
39
+ const staging = resolveCtxAccessor(prj, blockInfo.id, "staging");
40
+
41
+ const prodIncomplete = prod.calculated && prod.accessor === undefined;
42
+ const stagingIncomplete = staging.calculated && staging.accessor === undefined;
43
+
44
+ out.push({
45
+ blockId: blockInfo.id,
46
+ prodCtx: prod.accessor,
47
+ stagingCtx: staging.accessor,
48
+ prodIncomplete: prodIncomplete || undefined,
49
+ stagingIncomplete: stagingIncomplete || undefined,
50
+ });
51
+ }
52
+ return out;
53
+ }
54
+
55
+ interface CtxProbe {
56
+ /** True if the ctx-holder field exists in the project tree (block has started rendering). */
57
+ readonly calculated: boolean;
58
+ /** Resolved ui-ctx accessor (only when `.ok`). */
59
+ readonly accessor: PlTreeNodeAccessor | undefined;
60
+ }
61
+
62
+ function resolveCtxAccessor(
63
+ prj: PlTreeNodeAccessor,
64
+ blockId: string,
65
+ kind: "prod" | "staging",
66
+ ): CtxProbe {
67
+ const ctxField = kind === "prod" ? "prodCtx" : "stagingCtx";
68
+ const uiCtxField = kind === "prod" ? "prodUiCtx" : "stagingUiCtx";
69
+
70
+ const calculated =
71
+ prj.traverse({
72
+ field: projectFieldName(blockId, ctxField),
73
+ ignoreError: true,
74
+ pureFieldErrorToUndefined: true,
75
+ stableIfNotFound: true,
76
+ }) !== undefined;
77
+
78
+ const uiCtx = prj.traverseOrError({
79
+ field: projectFieldName(blockId, uiCtxField),
80
+ stableIfNotFound: true,
81
+ });
82
+
83
+ if (uiCtx === undefined || !uiCtx.ok) return { calculated, accessor: undefined };
84
+ return { calculated, accessor: uiCtx.value };
85
+ }
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { ModelServiceRegistry, Services } from "@milaboratories/pl-model-common";
10
10
  import { SpecDriver } from "@milaboratories/pf-spec-driver";
11
+ import { ColumnsCollectionDriverImpl } from "@milaboratories/columns-collection-driver";
11
12
  import { type MiLogger } from "@milaboratories/ts-helpers";
12
13
 
13
14
  export type ModelServiceOptions = {
@@ -19,5 +20,6 @@ export function createModelServiceRegistry(options: ModelServiceOptions) {
19
20
  PFrameSpec: () => new SpecDriver({ logger: options.logger }),
20
21
  PFrame: null,
21
22
  Dialog: null,
23
+ ColumnsCollection: () => new ColumnsCollectionDriverImpl(),
22
24
  });
23
25
  }