Skip to content

feat(model): primaryColumns + enrichFromPool for createPlDataTableV3#1606

Open
PoslavskySV wants to merge 2 commits into
mainfrom
sv/createpldatatable-v3-primary-enrichment
Open

feat(model): primaryColumns + enrichFromPool for createPlDataTableV3#1606
PoslavskySV wants to merge 2 commits into
mainfrom
sv/createpldatatable-v3-primary-enrichment

Conversation

@PoslavskySV

@PoslavskySV PoslavskySV commented May 3, 2026

Copy link
Copy Markdown
Member

Summary

Adds a simple-case sugar for the most common table pattern in blocks — show columns from my workflow output, plus joinable columns from the result pool as optional extras:

.outputWithStatus('mainTable', (ctx) =>
  createPlDataTableV3(ctx, {
    tableState: ctx.data.tableState,
    primaryColumns: ctx.outputs?.resolve('mainPf'),
    enrichFromPool: 'optional',
  }),
)

Three lines instead of the ~15 lines of ColumnCollectionBuilder plumbing this case currently requires. The advanced columns: { sources, anchors, selector } form is untouched and stays available for cases like antibody-tcr-lead-selection that need explicit anchor selection or custom-filtered sources.

Why

Today, blocks that just want to display their workflow output in a data table have two unappealing options:

  1. Use createPlDataTableV2 — flickers when result-pool label columns transition between "ready" and "not ready", because V2 returns undefined if any single label column isn't ready (see allPColumnsReady gate). This causes column headers to mount/unmount rapidly when the user adds metadata + re-runs upstream blocks.
  2. Use createPlDataTableV3 with columns: TableColumnVariant[] — works, but requires constructing variants with path, qualifications, originalId, isPrimary for every column. Bulk of the code is wrapping OutputColumnProvider(accessor).getAllColumns().map(snap => ({ column: ..., path: [], qualifications: { forHit: [], forQueries: {} }, originalId: snap.id, isPrimary: true })).

Neither matches the user's actual mental model: here are MY columns; please attach anything else from the pool that joins to them, optionally hideable.

What

New options on createPlDataTableOptionsV3

type createPlDataTableOptionsV3 = {
  tableState?: PlDataTableStateV2;

  // NEW
  primaryColumns?:
    | TreeNodeAccessor                                   // ctx.outputs?.resolve('mainPf')
    | PColumn<PColumnDataUniversal>[]                    // accessor.getPColumns()
    | TableColumnVariant[];                              // power-user explicit form
  enrichFromPool?:
    | false | true
    | 'optional' | 'hidden' | 'default'
    | { visibility?, exclude?, maxHops?, mode? };

  // EXISTING — unchanged
  columns?: Nil | DiscoverTableColumnOptions | TableColumnVariant[];
  filters?: PlDataTableFilters;
  sorting?: PTableSorting[];
  primaryJoinType?: 'inner' | 'full';
  labelsOptions?: DeriveLabelsOptions;
  displayOptions?: ColumnsDisplayOptions;
};

primaryColumns and columns are mutually exclusive — passing both throws.

Relaxation on TableColumnVariant

path, qualifications, originalId, and isPrimary are now all optional with documented defaults ([], { forHit: [], forQueries: {} }, column.id, true). For the common case only column is required.

Implementation strategy

  • primaryColumns is normalized to variants tagged isPrimary: true. TreeNodeAccessor is wrapped via OutputColumnProvider (preserves per-column dataStatus for partial-data rendering).
  • enrichFromPool runs anchored discovery via ColumnCollectionBuilder, with primary columns registered as the FIRST source. The builder's existing NativePObjectId dedup (first-source-wins) handles the case where the workflow output is also re-published to the result pool — no manual exclusion needed.
  • Each enriched variant gets the configured visibility set as a pl7.app/table/visibility annotation on its spec. User-supplied displayOptions.visibility rules still take precedence per the existing first-match-wins evaluation in evaluateRules.

Documentation

  • JSDoc with @default on every option.
  • Top-of-function block for createPlDataTableV3 showing common usage.
  • Documented ColumnsSelectorConfig, ColumnsDisplayOptions, ColumnOrderRule, ColumnVisibilityRule, ColumnMatcher, DiscoverTableColumnOptions, and the internal pipeline functions (splitDiscoveredColumns, annotateColumnGroups, buildSecondaryGroups, etc.).

Backward compatibility

Verified clean. Every change is either additive (new optional fields), a relaxation (required → optional), or a type widening (DiscoveredPColumnId → DiscoveredPColumnId | PObjectId, where DiscoveredPColumnId still assignable). All existing callers in the monorepo compile and run unchanged. Specifically:

  • clonotype-browser (uses columns: { ... } discovery and columns: TableColumnVariant[]) — untouched.
  • antibody-tcr-lead-selection (uses columns: { sources, anchors, selector } with custom-filtered pool sources, custom anchor selection, multi-rule visibility/ordering, labelsOptions.formatters.linker) — every advanced lever they use is preserved.
  • etc/blocks/table-test — uses columns: { ... } form, untouched.

TooltipableColumn.id widened from DiscoveredPColumnId to PObjectId | DiscoveredPColumnId. Backward-compatible because DiscoveredPColumnId extends PObjectId via branding — both still accepted.

Test plan

  • pnpm types:check clean for @platforma-sdk/model.
  • pnpm test — 384 passed (374 existing + 10 new).
  • pnpm build clean (declaration generation works).
  • Local block validation: building the dev SDK in clonotype-distribution, clonotype-browser, and antibody-tcr-lead-selection (in progress separately).
  • Manual UI verification once dev blocks are loaded into desktop app.

Files changed

  • sdk/model/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts — main implementation + extensive JSDoc.
  • sdk/model/src/components/PlDataTable/createPlDataTable/discoverColumns.ts — JSDoc only.
  • sdk/model/src/components/PlDataTable/createPlDataTable/utils.ts — single-field type widening on TooltipableColumn.id.
  • sdk/model/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.test.ts — new test file (10 tests).
  • .changeset/createpldatatablev3-primary-enrichment.md — minor bump for @platforma-sdk/model.

Greptile Summary

This PR adds primaryColumns and enrichFromPool as ergonomic sugar on createPlDataTableV3, reducing the typical "show my workflow output + joinable pool extras" pattern from ~15 lines of ColumnCollectionBuilder plumbing to 3. All existing callers are unaffected; TableColumnVariant fields are relaxed to optional with sensible defaults, and the advanced columns: { sources, anchors, selector } form is preserved unchanged.

Confidence Score: 4/5

Safe to merge — no P0/P1 findings; two P2 issues are low-impact edge cases.

Only P2 findings present: a fragile duck-typing check to distinguish PColumn[] from TableColumnVariant[], and a mutual-exclusion guard that incorrectly fires when columns: null (a valid Nil value) is combined with primaryColumns. Neither affects the primary happy-path flows, which are covered by 10 new tests.

createPlDataTableV3.ts — the normalizePrimaryColumns array-type discriminant and the mutual-exclusion null check.

Important Files Changed

Filename Overview
sdk/model/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts Main implementation — adds primaryColumns + enrichFromPool sugar with well-structured normalization pipeline; two minor issues: fragile duck-typing to distinguish input array types, and mutual-exclusion guard fires on columns: null (valid Nil value).
sdk/model/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.test.ts New test file — 10 unit tests covering normalizeEnrichmentConfig shorthands and normalizeTableColumnVariant defaults; well-structured and thorough for the targeted units.
sdk/model/src/components/PlDataTable/createPlDataTable/discoverColumns.ts JSDoc additions only — no logic changes; docs accurately describe the advanced columns: { sources, anchors, selector } form.
sdk/model/src/components/PlDataTable/createPlDataTable/utils.ts Single backward-compatible type widening on TooltipableColumn.id from DiscoveredPColumnId to PObjectId
.changeset/createpldatatablev3-primary-enrichment.md Changeset file — correctly marked as minor bump for @platforma-sdk/model, with accurate description of all changes.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
sdk/model/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts:719-721
**Fragile duck-typing to distinguish `PColumn[]` from `TableColumnVariant[]`**

The heuristic `"column" in first` relies on `PColumn` not having a `column` property. If `PColumn` ever gains such a field, this silently misclassifies the input — treating a `PColumn[]` as a `TableColumnVariant[]` and wrapping it incorrectly. Consider using `"spec" in first && "data" in first` instead, which targets structural requirements of `PColumn` that are absent from `TableColumnVariant`.

```suggestion
  const first = input[0] as object;
  if ("spec" in first && "data" in first) {
    return new ArrayColumnProvider(input as PColumn<PColumnDataUniversal>[])
      .getAllColumns()
      .map(snapshotToPrimaryVariant);
  }
  return (input as TableColumnVariant[]).map(normalizeTableColumnVariant);
```

### Issue 2 of 2
sdk/model/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts:461-465
**Mutual-exclusion check fires on `columns: null`**

`Nil` is `undefined | null`, and `columns` is typed as `Nil | DiscoverTableColumnOptions | TableColumnVariant[]`. Passing `{ primaryColumns: ..., columns: null }` trips the guard (`null !== undefined` is `true`) and throws — even though `null` is a documented valid value meaning "no columns". Using `!isNil(options.columns)` would let `null` fall through correctly.

```suggestion
  if (options.primaryColumns !== undefined && !isNil(options.columns)) {
    throw new Error(
      "createPlDataTableV3: `primaryColumns` and `columns` are mutually exclusive — pass one or the other.",
    );
  }
```

Reviews (1): Last reviewed commit: "feat(model): primaryColumns + enrichFrom..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Adds a simple-case sugar for the most common table pattern: "show columns
from my workflow output, plus joinable columns from the result pool as
optional extras." Existing `columns: { sources, anchors, selector }`
form unchanged.

```ts
.outputWithStatus('mainTable', (ctx) =>
  createPlDataTableV3(ctx, {
    tableState: ctx.data.tableState,
    primaryColumns: ctx.outputs?.resolve('mainPf'),
    enrichFromPool: 'optional',
  }),
)
```

`primaryColumns` accepts `TreeNodeAccessor`, `PColumn[]`, or
`TableColumnVariant[]`. `enrichFromPool` accepts `false`, `true`, a
visibility shorthand (`'optional' | 'hidden' | 'default'`), or a full
options object. Mutually exclusive with `columns`.

Other changes:
- TableColumnVariant: `path`, `qualifications`, `originalId`, `isPrimary`
  are now optional with sensible defaults. Common case needs only `column`.
- Extensive JSDoc on createPlDataTableV3 and dependent types.
- TooltipableColumn.id widened to `PObjectId | DiscoveredPColumnId`
  (DiscoveredPColumnId still assignable — backward compatible).

Backward-compatible: existing callers compile and run unchanged.
@changeset-bot

changeset-bot Bot commented May 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 52c2be1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@platforma-sdk/model Minor
@milaboratories/pl-middle-layer Patch
@milaboratories/pl-mcp-server Major
@milaboratories/uikit Patch
@platforma-sdk/test Minor
@platforma-sdk/ui-vue Minor
@milaboratories/milaboratories.monetization-test.model Patch
@milaboratories/milaboratories.monetization-test.ui Patch
@milaboratories/milaboratories.ui-examples.model Patch
@milaboratories/milaboratories.ui-examples.ui Patch
@milaboratories/milaboratories.ui-examples Patch
@milaboratories/milaboratories.pool-explorer.model Patch
@milaboratories/milaboratories.pool-explorer Patch
@platforma-sdk/pl-cli Patch
@milaboratories/milaboratories.pool-explorer.ui Patch
@milaboratories/milaboratories.monetization-test Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances createPlDataTableV3 by adding primaryColumns and enrichFromPool options, which streamline the creation of data tables from workflow outputs and joinable pool columns. The changes include new normalization and discovery logic, updated type definitions, extensive JSDoc documentation, and corresponding unit tests. I have no feedback to provide.

Comment on lines +719 to +721

// ---------------------------------------------------------------------------
// Internal pipeline (split / annotate / build defs / etc.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Fragile duck-typing to distinguish PColumn[] from TableColumnVariant[]

The heuristic "column" in first relies on PColumn not having a column property. If PColumn ever gains such a field, this silently misclassifies the input — treating a PColumn[] as a TableColumnVariant[] and wrapping it incorrectly. Consider using "spec" in first && "data" in first instead, which targets structural requirements of PColumn that are absent from TableColumnVariant.

Suggested change
// ---------------------------------------------------------------------------
// Internal pipeline (split / annotate / build defs / etc.)
const first = input[0] as object;
if ("spec" in first && "data" in first) {
return new ArrayColumnProvider(input as PColumn<PColumnDataUniversal>[])
.getAllColumns()
.map(snapshotToPrimaryVariant);
}
return (input as TableColumnVariant[]).map(normalizeTableColumnVariant);
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/model/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts
Line: 719-721

Comment:
**Fragile duck-typing to distinguish `PColumn[]` from `TableColumnVariant[]`**

The heuristic `"column" in first` relies on `PColumn` not having a `column` property. If `PColumn` ever gains such a field, this silently misclassifies the input — treating a `PColumn[]` as a `TableColumnVariant[]` and wrapping it incorrectly. Consider using `"spec" in first && "data" in first` instead, which targets structural requirements of `PColumn` that are absent from `TableColumnVariant`.

```suggestion
  const first = input[0] as object;
  if ("spec" in first && "data" in first) {
    return new ArrayColumnProvider(input as PColumn<PColumnDataUniversal>[])
      .getAllColumns()
      .map(snapshotToPrimaryVariant);
  }
  return (input as TableColumnVariant[]).map(normalizeTableColumnVariant);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +461 to +465
* direct columns (the typical case).
* @default []
*/
readonly path?: { linker: ColumnSnapshot<PObjectId> }[];
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Mutual-exclusion check fires on columns: null

Nil is undefined | null, and columns is typed as Nil | DiscoverTableColumnOptions | TableColumnVariant[]. Passing { primaryColumns: ..., columns: null } trips the guard (null !== undefined is true) and throws — even though null is a documented valid value meaning "no columns". Using !isNil(options.columns) would let null fall through correctly.

Suggested change
* direct columns (the typical case).
* @default []
*/
readonly path?: { linker: ColumnSnapshot<PObjectId> }[];
/**
if (options.primaryColumns !== undefined && !isNil(options.columns)) {
throw new Error(
"createPlDataTableV3: `primaryColumns` and `columns` are mutually exclusive — pass one or the other.",
);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/model/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts
Line: 461-465

Comment:
**Mutual-exclusion check fires on `columns: null`**

`Nil` is `undefined | null`, and `columns` is typed as `Nil | DiscoverTableColumnOptions | TableColumnVariant[]`. Passing `{ primaryColumns: ..., columns: null }` trips the guard (`null !== undefined` is `true`) and throws — even though `null` is a documented valid value meaning "no columns". Using `!isNil(options.columns)` would let `null` fall through correctly.

```suggestion
  if (options.primaryColumns !== undefined && !isNil(options.columns)) {
    throw new Error(
      "createPlDataTableV3: `primaryColumns` and `columns` are mutually exclusive — pass one or the other.",
    );
  }
```

How can I resolve this? If you propose a fix, please make it concise.

@codecov

codecov Bot commented May 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 23.72881% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.90%. Comparing base (1bfca16) to head (52c2be1).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...DataTable/createPlDataTable/createPlDataTableV3.ts 23.72% 45 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1606      +/-   ##
==========================================
- Coverage   53.91%   52.90%   -1.01%     
==========================================
  Files         265      269       +4     
  Lines       15031    15332     +301     
  Branches     3162     3240      +78     
==========================================
+ Hits         8104     8112       +8     
- Misses       5889     6183     +294     
+ Partials     1038     1037       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The pool walk inside `discoverEnrichmentColumns` calls into pframes-rs-wasm
through `pframeSpec`. Discovered in field testing: when the result pool
contains certain column shapes (e.g. after a parameter change that
restructures the workflow output), the WASM layer can trap with
"table index is out of bounds", which propagates as a thrown error and
aborts whatever task is evaluating the model — including
mutate-block-storage, blocking parameter changes entirely.

Catch the error, log a warning, and return an empty enrichment list.
The table still renders with its primary columns; only the optional
pool-discovered extras are missing until the underlying WASM bug is
fixed. The collection is still disposed via the existing finally.

The WASM trap itself needs a separate fix in pframes-rs-wasm; this
change is purely a robustness boundary at the SDK layer.
@PoslavskySV

Copy link
Copy Markdown
Member Author

Hardening commit added: 52c2be10e

Field testing surfaced a WASM trap (RuntimeError: table index is out of bounds) inside pframes-rs-wasm when enrichFromPool discovery walks a pool whose column shape just changed (e.g. the user re-runs with a different grouping/temporal variable, restructuring the workflow output). The trap propagates through mutate-block-storage, blocking parameter changes for the user.

The WASM bug needs a separate fix in pframes-rs-wasm — but the SDK now wraps discoverEnrichmentColumns in try/catch, logs a warning, and falls back to primary-only rendering. Block stays usable; only the optional pool-discovered extras are missing until the underlying WASM bug is fixed.

This is a defensive boundary, not a workaround for incorrect logic in our own code. No behavior change in the success path.

Comment on lines +116 to +117
* - `false` (default) — no enrichment.
* - `true` — equivalent to `'optional'`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those two options only complicate contract without benefits

* Mutually exclusive with `columns`. When neither is set or this resolves
* to an empty list, the function returns `undefined`.
*/
primaryColumns?: TreeNodeAccessor | PColumn<PColumnDataUniversal>[] | TableColumnVariant[];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have mechanism for this, you can mark isPrimary = true in columns or you can make this columns as anchors

return {
column: {
id: discoveredId,
spec: withVisibilityAnnotation(v.column.spec, visibility),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong, visibility shouldn't influence to id. ID concept is uniquity of data, not uniquity of representation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants