Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e101a63
feat: per-instance trace label via customBlockLabel || defaultBlockLabel
PaulNewling May 18, 2026
cff9b24
test: regression coverage for PR #9 dedup invariants
PaulNewling May 18, 2026
89d6b71
test: cross-process byte-stability for Python outputs
PaulNewling May 18, 2026
c9dddea
chore: ruff format on test_determinism.py
PaulNewling May 18, 2026
7d67a2a
test: parametrize byte-stability across peptide/antibody
PaulNewling May 18, 2026
69bb620
chore: bump packageManager to pnpm@9.15.0
PaulNewling May 18, 2026
eebd6c3
fix(model): backfill label fields via a new migration step
PaulNewling May 18, 2026
87fdbc8
docs(model): tighten migration-chain comments
PaulNewling May 18, 2026
cef0f4d
test: assert aCols.length > 0 before index access
PaulNewling May 18, 2026
6e0b75e
fix(ui): expose own properties as color/filter/group dimensions
PaulNewling May 19, 2026
3ac898d
fix(workflow): static trace-label fallback when both labels empty
PaulNewling May 19, 2026
4ae43e3
docs(workflow): tighten trace-label fallback comment
PaulNewling May 19, 2026
c70882e
chore: bump @platforma-sdk/block-tools 2.7.23 → 2.8.1
PaulNewling May 19, 2026
a41996d
test(model): tdd resolveSubtitle + resolveTraceLabel helpers
PaulNewling May 20, 2026
29c1366
refactor(model): project single traceLabel via resolveTraceLabel helper
PaulNewling May 20, 2026
c027e38
refactor(workflow): consume args.traceLabel from model resolver
PaulNewling May 20, 2026
d4eb095
test(model): lock Ver_2026_05_18 backfill behaviour
PaulNewling May 20, 2026
83536eb
test: stage SC IG fastq fixtures so dedup test runs in CI
PaulNewling May 20, 2026
9f20516
ci: enable block-level tests in CI + document via changeset
PaulNewling May 20, 2026
aa281ac
ci(changeset): consolidate three changesets into one
PaulNewling May 20, 2026
06dc51c
docs: clear-writing pass on PR comments
PaulNewling May 20, 2026
8072fdc
chore: pin root packageManager to pnpm@9.15.0
PaulNewling May 20, 2026
0a7a879
fix(test): use mutateBlockStorage for V3 sequence-properties blocks
PaulNewling May 20, 2026
e5336f6
fix(test): convert helpers.ts to V3 mutateBlockStorage
PaulNewling May 20, 2026
988068d
refactor(test): drop 'as unknown as' on tableState via V5 literal
PaulNewling May 20, 2026
1a607b5
test: revert two-instance dedup test to it.todo
PaulNewling May 20, 2026
de52bcb
docs: remove unneeded comment
PaulNewling May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/sequence-properties-trace-label-and-meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@platforma-open/milaboratories.sequence-properties.workflow': minor
'@platforma-open/milaboratories.sequence-properties.model': minor
'@platforma-open/milaboratories.sequence-properties.ui': minor
'@platforma-open/milaboratories.sequence-properties': minor
'@platforma-open/milaboratories.sequence-properties.test': patch
---

Per-instance trace label, broader plot pickers, locked-in test coverage.

- **Trace label is per-instance.** The workflow's `pl7.app/trace.label` resolves to `customBlockLabel || defaultBlockLabel || "Sequence Properties"` (centralised in `model/src/label.ts`). Two sequence-properties blocks on the same dataset show distinguishable entries in Lead Selection and other downstream pickers once the user customises the `PlBlockPage` subtitle. Same pattern as clonotype-clustering and titeseq-analysis PR #13.
- **Scatter and Histogram metadata pickers accept own-block columns.** Filter, Grouping/Color, Highlight, Size, Tab, Tooltip, Label, and Additional-curves now treat every column in the property pframe as a candidate — own scalars and upstream metadata alike. Users can color the Property Relationships scatter by Aromaticity while plotting Charge vs Hydrophobicity. X/Y axis defaults unchanged.
- **Migration backfill.** A new `Ver_2026_05_18` step fills the new label fields onto projects tagged at the deployed `Ver_2026_05_05`, preserving any interim-deployed value via `?? ""`. Without the split, already-V2 projects would skip the migration and the workflow would receive `args.customBlockLabel === undefined`.
- **Test coverage.** Model vitest locks the resolution chain (6 cases) and the migration backfill (4 cases). A subprocess-based Python byte-compare test guards Python output determinism. `build.yaml` enables `test: true` so block-level tests exercise on every PR.
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
build-script-name: 'build'
pnpm-recursive-build: false

test: false
test: true
test-script-name: 'test'
pnpm-recursive-tests: false
team-id: 'ciplopen'
Expand Down
2 changes: 1 addition & 1 deletion block/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,5 @@
"devDependencies": {
"@platforma-sdk/block-tools": "catalog:"
},
"packageManager": "pnpm@9.12.0"
"packageManager": "pnpm@9.15.0"
}
7 changes: 5 additions & 2 deletions model/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"fmt": "ts-builder format",
"watch": "ts-builder build --target block-model --watch",
"build": "ts-builder build --target block-model && block-tools build-model",
"check": "ts-builder check --target block-model"
"check": "ts-builder check --target block-model",
"test": "vitest --run --passWithNoTests",
"test:watch": "vitest"
},
"dependencies": {
"@milaboratories/graph-maker": "catalog:",
Expand All @@ -27,7 +29,8 @@
"devDependencies": {
"@milaboratories/ts-builder": "catalog:",
"@milaboratories/ts-configs": "catalog:",
"@platforma-sdk/block-tools": "catalog:"
"@platforma-sdk/block-tools": "catalog:",
"vitest": "catalog:"
},
"peerDependencies": {
"@types/node": "*",
Expand Down
63 changes: 63 additions & 0 deletions model/src/dataModel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Tests drive the migration callbacks directly because @platforma-sdk/model's
// DataModelBuilder exposes no introspection / external-apply API. The builder
// wiring in dataModel.ts (the two `.migrate(...)` calls) is only exercised
// when a real block loads — these unit tests would still pass if someone
// deleted a `.migrate(...)` line while keeping the named callback exported.

import { describe, expect, it } from "vitest";
import type { BlockData, BlockDataV1, BlockDataV2 } from "./types";
import { migrateV1toV2, migrateV2toV2_1 } from "./dataModel";

const tableState = {
pTableParams: {
defaultFilters: null,
filters: null,
hiddenColIds: null,
sorting: [],
sourceId: null,
},
stateCache: [],
version: 6,
} as BlockDataV2["tableState"];

const v2Graph: Pick<BlockDataV2, "graphStateScatter" | "graphStateHistogram"> = {
graphStateScatter: { currentTab: null, template: "dots", title: "Property Relationships" },
graphStateHistogram: {
currentTab: null,
layersSettings: { bins: { fillColor: "#99e099" } },
template: "bins",
title: "Property Distribution",
},
};

describe("blockDataModel Ver_2026_05_18 backfill", () => {
it("backfills both label fields to '' on a bare V2 payload", () => {
const v2: BlockDataV2 = { tableState, ...v2Graph };
const upgraded: BlockData = migrateV2toV2_1(v2);
expect(upgraded.customBlockLabel).toBe("");
expect(upgraded.defaultBlockLabel).toBe("");
});

it("preserves an interim-deployed customBlockLabel", () => {
const v2: BlockDataV2 = { tableState, ...v2Graph, customBlockLabel: "X" };
const upgraded: BlockData = migrateV2toV2_1(v2);
expect(upgraded.customBlockLabel).toBe("X");
expect(upgraded.defaultBlockLabel).toBe("");
});

it("preserves an interim-deployed defaultBlockLabel", () => {
const v2: BlockDataV2 = { tableState, ...v2Graph, defaultBlockLabel: "Y" };
const upgraded: BlockData = migrateV2toV2_1(v2);
expect(upgraded.defaultBlockLabel).toBe("Y");
expect(upgraded.customBlockLabel).toBe("");
});

it("runs the full V1 → V2 → V2.1 chain on legacy data", () => {
const v1: BlockDataV1 = { tableState, defaultBlockLabel: "Old" };
const upgraded: BlockData = migrateV2toV2_1(migrateV1toV2(v1));
expect(upgraded.defaultBlockLabel).toBe("Old");
expect(upgraded.customBlockLabel).toBe("");
expect(upgraded.graphStateScatter).toBeDefined();
expect(upgraded.graphStateHistogram).toBeDefined();
});
});
30 changes: 24 additions & 6 deletions model/src/dataModel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { GraphMakerState } from "@milaboratories/graph-maker";
import { createPlDataTableStateV2, DataModelBuilder } from "@platforma-sdk/model";
import type { BlockData, BlockDataV1 } from "./types";
import type { BlockData, BlockDataV1, BlockDataV2 } from "./types";

const DEFAULT_SCATTER_STATE: GraphMakerState = {
title: "Property Relationships",
Expand All @@ -17,15 +17,33 @@ const DEFAULT_HISTOGRAM_STATE: GraphMakerState = {
},
};

export const migrateV1toV2 = (v1: BlockDataV1): BlockDataV2 => ({
...v1,
graphStateScatter: { ...DEFAULT_SCATTER_STATE },
graphStateHistogram: { ...DEFAULT_HISTOGRAM_STATE },
});

export const migrateV2toV2_1 = (v2: BlockDataV2): BlockData => ({
...v2,
defaultBlockLabel: v2.defaultBlockLabel ?? "",
customBlockLabel: v2.customBlockLabel ?? "",
});

export const blockDataModel = new DataModelBuilder()
.from<BlockDataV1>("Ver_2026_04_28")
.migrate<BlockData>("Ver_2026_05_05", (v1) => ({
...v1,
graphStateScatter: { ...DEFAULT_SCATTER_STATE },
graphStateHistogram: { ...DEFAULT_HISTOGRAM_STATE },
}))
// Already-deployed step. Future field additions must go into a new step
// below — editing this body has no effect on projects already tagged
// Ver_2026_05_05 (DataModelBuilder skips matching-version migrations).
.migrate<BlockDataV2>("Ver_2026_05_05", migrateV1toV2)
// Backfills label fields onto V2-tagged projects. `?? ""` preserves any
// interim-deployed value; missing fields default to "". The args
// projection (resolveTraceLabel in label.ts) requires both fields to be
// strings, never undefined.
.migrate<BlockData>("Ver_2026_05_18", migrateV2toV2_1)
.init(() => ({
tableState: createPlDataTableStateV2(),
defaultBlockLabel: "",
customBlockLabel: "",
graphStateScatter: { ...DEFAULT_SCATTER_STATE },
graphStateHistogram: { ...DEFAULT_HISTOGRAM_STATE },
}));
4 changes: 3 additions & 1 deletion model/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createPlDataTableV2,
} from "@platforma-sdk/model";
import { blockDataModel } from "./dataModel";
import { resolveSubtitle, resolveTraceLabel } from "./label";
import type { BlockArgs, WorkflowInfo } from "./types";

export type * from "@milaboratories/helpers";
Expand Down Expand Up @@ -41,6 +42,7 @@ export const platforma = BlockModelV3.create(blockDataModel)
}
return {
inputAnchor: data.inputAnchor,
traceLabel: resolveTraceLabel(data),
};
})
.output("inputOptions", (ctx) => ctx.resultPool.getOptions(inputAnchorSpecs))
Expand Down Expand Up @@ -107,7 +109,7 @@ export const platforma = BlockModelV3.create(blockDataModel)
return pCols.map((c) => ({ columnId: c.id, spec: c.spec }) satisfies PColumnIdAndSpec);
})
.title(() => "Sequence Properties")
.subtitle((ctx) => ctx.data.defaultBlockLabel ?? "")
.subtitle((ctx) => resolveSubtitle(ctx.data))
.sections(() => [
{ type: "link", href: "/", label: "Main" },
{ type: "link", href: "/scatter", label: "Property Relationships" },
Expand Down
62 changes: 62 additions & 0 deletions model/src/label.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import type { BlockData } from "./types";
import { resolveSubtitle, resolveTraceLabel } from "./label";

const base: Omit<BlockData, "customBlockLabel" | "defaultBlockLabel"> = {
tableState: {
pTableParams: {
defaultFilters: null,
filters: null,
hiddenColIds: null,
sorting: [],
sourceId: null,
},
stateCache: [],
version: 6,
} as BlockData["tableState"],
graphStateScatter: {
currentTab: null,
template: "dots",
title: "Property Relationships",
} as BlockData["graphStateScatter"],
graphStateHistogram: {
currentTab: null,
layersSettings: { bins: { fillColor: "#99e099" } },
template: "bins",
title: "Property Distribution",
} as BlockData["graphStateHistogram"],
};

const make = (custom: string, def: string): BlockData => ({
...(base as BlockData),
customBlockLabel: custom,
defaultBlockLabel: def,
});

describe("resolveSubtitle", () => {
it("uses customBlockLabel when set", () => {
expect(resolveSubtitle(make("My label", "Dataset"))).toBe("My label");
});

it("falls back to defaultBlockLabel when customBlockLabel is empty", () => {
expect(resolveSubtitle(make("", "Dataset"))).toBe("Dataset");
});

it("returns empty string when both label fields are empty", () => {
expect(resolveSubtitle(make("", ""))).toBe("");
});
});

describe("resolveTraceLabel", () => {
it("uses customBlockLabel when set", () => {
expect(resolveTraceLabel(make("My label", "Dataset"))).toBe("My label");
});

it("falls back to defaultBlockLabel when customBlockLabel is empty", () => {
expect(resolveTraceLabel(make("", "Dataset"))).toBe("Dataset");
});

it("falls back to the block-type default when both label fields are empty", () => {
expect(resolveTraceLabel(make("", ""))).toBe("Sequence Properties");
});
});
17 changes: 17 additions & 0 deletions model/src/label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { BlockData } from "./types";

const STATIC_FALLBACK = "Sequence Properties";

// Subtitle resolution for the PlBlockPage. Empty string is a valid result
// (the page renders the title alone).
export function resolveSubtitle(data: BlockData): string {
return data.customBlockLabel || data.defaultBlockLabel;
}

// Trace-label resolution for the workflow's pl7.app/trace.label. Same chain
// as resolveSubtitle plus a static block-type last-resort, so automated
// pipelines that run before the UI populates defaultBlockLabel still emit
// a non-empty label downstream.
export function resolveTraceLabel(data: BlockData): string {
return data.customBlockLabel || data.defaultBlockLabel || STATIC_FALLBACK;
}
20 changes: 16 additions & 4 deletions model/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,31 @@ import type { PlDataTableStateV2, PlRef } from "@platforma-sdk/model";
export type BlockDataV1 = {
inputAnchor?: PlRef;
tableState: PlDataTableStateV2;
// UI-only state. Tracks the selected input dataset's label so the block
// subtitle can reflect it — populated by the UI watcher in app.ts. Not
// projected into BlockArgs because the workflow does not consume it.
// Historically optional UI-only state. Required in the current BlockData
// shape; consumed by the label helpers in label.ts (resolveSubtitle for
// the PlBlockPage subtitle, resolveTraceLabel for the workflow trace).
defaultBlockLabel?: string;
};

export type BlockData = BlockDataV1 & {
// V2 shape — what the deployed Ver_2026_05_05 migration produces. Input to
// the new Ver_2026_05_18 step that backfills the label fields. Both label
// fields are optional here so the V2→V2.1 migration can read-or-default any
// value an interim deployment may have written.
export type BlockDataV2 = Omit<BlockDataV1, "defaultBlockLabel"> & {
defaultBlockLabel?: string;
customBlockLabel?: string;
graphStateScatter: GraphMakerState;
graphStateHistogram: GraphMakerState;
};

export type BlockData = Omit<BlockDataV2, "defaultBlockLabel"> & {
defaultBlockLabel: string;
customBlockLabel: string;
};

export type BlockArgs = {
inputAnchor: PlRef;
traceLabel: string;
};

export type WorkflowMode =
Expand Down
8 changes: 8 additions & 0 deletions model/vitest.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
watch: false,
testTimeout: 5000,
},
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@
"oxfmt": "*",
"oxlint": "*"
},
"packageManager": "pnpm@10.33.2"
"packageManager": "pnpm@9.15.0"
}
Loading
Loading