Skip to content

Commit 9d27224

Browse files
AStaroverovclaude
andcommitted
Namespace exported columns by content hash instead of blockId
Stamp pl7.app/contentHash (base32 of the source table's CanonicalID) on the exported property columns instead of the per-block blockId, and drop the blockId-keyed trace step id. Identical results across blocks/projects now produce content-identical columns that dedupe downstream, while different results stay distinct. Mirrors the humanization-score approach. The Python step and Parquet import already deduped; this extends it to the export. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f0ea797 commit 9d27224

4 files changed

Lines changed: 46 additions & 26 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@platforma-open/milaboratories.sequence-properties.workflow": patch
3+
---
4+
5+
Namespace exported property columns by a content hash of the source table (pl7.app/contentHash, derived from the backend CanonicalID) instead of the per-block blockId, and drop the blockId-keyed trace step id. Identical results across blocks/projects now produce content-identical columns that dedupe downstream instead of being made unique per block; different results stay distinct. The heavy Python step and Parquet import already deduped; this extends dedup to the exported pframe identity.

test/src/wf.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ describe('edge cases', () => {
127127
describe('downstream consumption', () => {
128128
it.todo('score columns are discoverable by a downstream block via the result pool');
129129
it.todo('output PColumns carry pl7.app/trace stamped with this block\'s label');
130-
it.todo('export PFrame stamps blockId on score column domains');
130+
it.todo('export PFrame stamps pl7.app/contentHash on score column domains');
131131
});
132132

133133
// ---------------------------------------------------------------------------
@@ -160,6 +160,6 @@ describe('dedup', () => {
160160
describe('cross-block composition', () => {
161161
it.todo('canary: real MiXCR fastq → sequence-properties detects sc IG and emits scores');
162162
it.todo('end-to-end: peptide-extraction → sequence-properties → lead-selection');
163-
it.todo('two sequence-properties instances in one project disambiguate via blockId');
163+
it.todo('two sequence-properties instances on identical input share columns via pl7.app/contentHash');
164164
it.todo('VDJ chain: full-coverage MiXCR → seq-properties → lead-selection');
165165
});

workflow/src/columns.lib.tengo

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
// Returns `[{column, id, naRegex,
77
// allowNA, spec}, ...]` with feature-
88
// specific domains and annotations.
9-
// No blockId, no isOutput, no trace
9+
// No content tag, no isOutput, no trace
1010
// — those are stamped on by the caller
1111
// at pframe-build time.
1212
// - `aaFractionColumn()` — 2-axis AA-fraction column descriptor
1313
// (peptide mode only).
1414
// - `cloneSpec(spec, dExtras, aExtras)` — spec-cloning helper used by the
15-
// caller to layer blockId / isOutput /
15+
// caller to layer contentHash / isOutput /
1616
// any other per-consumer overrides.
1717
//
1818
// `args` shape:
@@ -70,7 +70,7 @@ makeCol := func(tsvCol, valName, valueType, label, domain, annotations) {
7070
}
7171

7272
// ---------------------------------------------------------------------------
73-
// Canonical column lists per scope. No `toPlot`, no `blockId` — the public
73+
// Canonical column lists per scope. No `toPlot`, no content tag — the public
7474
// getters layer those on per consumer.
7575
// ---------------------------------------------------------------------------
7676

@@ -386,7 +386,7 @@ buildColumns := func(args) {
386386
// ---------------------------------------------------------------------------
387387
// Spec-cloning helper. Builds a fresh spec dict with optional domain and
388388
// annotation extras. Callers use this to layer per-consumer overrides
389-
// (e.g. `pl7.app/blockId` in domain, `pl7.app/isOutput` in annotations).
389+
// (e.g. `pl7.app/contentHash` in domain, `pl7.app/isOutput` in annotations).
390390
// ---------------------------------------------------------------------------
391391

392392
cloneSpec := func(spec, domainExtras, annotationExtras) {

workflow/src/main.tpl.tengo

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ canonicalJsonResource := func(value) {
3131
return smart.createValueResource(constants.RTYPE_JSON, canonical.encode(value))
3232
}
3333

34+
// Content tag for exported columns, replacing the per-block blockId. We use the
35+
// backend's own content key of the source table (info().CanonicalID, base32),
36+
// which is deterministic for identical content: identical results across blocks
37+
// share the same tag (columns dedupe instead of being made unique per block),
38+
// while different results get different tags (no collision).
39+
contentHash := func(table) {
40+
i := table.info()
41+
if !is_undefined(i.CanonicalID) {
42+
return ll.base32Encode(i.CanonicalID)
43+
}
44+
// Field reference: dereference to its value resource.
45+
ri := smart.resource(i.Value).info()
46+
return ll.base32Encode(ri.CanonicalID)
47+
}
48+
3449
REQUIRED_FEATURES := ["FR1", "CDR1", "FR2", "CDR2", "FR3", "CDR3", "FR4"]
3550

3651
detectMode := func(axisSpec) {
@@ -138,7 +153,6 @@ wf.prepare(func(args) {
138153
})
139154

140155
wf.body(func(args) {
141-
blockId := wf.blockId().getDataAsJson()
142156
bundle := args.columns
143157
datasetSpec := bundle.getSpec(args.inputAnchor)
144158

@@ -431,40 +445,41 @@ wf.body(func(args) {
431445
}, { splitDataAndSpec: true, cpu: 1, mem: "4GiB" })
432446
}
433447

434-
// Stamp every column's spec with `pl7.app/blockId` in domain (for run
435-
// attribution) and the upstream trace (so downstream blocks see the full
436-
// provenance). The same pFrame is published both as the block's UI output
437-
// and as the result-pool export — one canonical resource, two consumers.
438-
// The trace is also how the block's own UI distinguishes our property
439-
// columns from upstream metadata columns mixed into propertiesPfHandle.
440-
// Per-instance trace label. Resolution lives in the model
441-
// (resolveTraceLabel in model/src/label.ts) — the model projects a single
442-
// string into args so the workflow does not know about customBlockLabel
443-
// vs defaultBlockLabel vs the static fallback. The fallback chain
444-
// itself — customBlockLabel || defaultBlockLabel || "Sequence Properties"
445-
// — matches clonotype-clustering and titeseq-analysis PR #13; only the
446-
// location of the resolution differs (model here, inline-Tengo there).
448+
// Stamp every column's spec with a content tag (pl7.app/contentHash, derived
449+
// from the source table's CanonicalID instead of the per-block blockId) and
450+
// the upstream trace. Identical results across blocks share the tag (columns
451+
// dedupe) while different results stay distinct. The same pFrame is published
452+
// both as the block's UI output and as the result-pool export. The trace also
453+
// lets the block's UI distinguish our property columns from upstream metadata
454+
// columns mixed into propertiesPfHandle.
455+
//
456+
// The trace step is no longer keyed on blockId (the block is no longer the
457+
// identity of the result; its content is). `id` is optional in the trace
458+
// schema; omitting it keeps the provenance step identical across
459+
// content-identical runs. Per-instance trace label resolution lives in the
460+
// model (resolveTraceLabel in model/src/label.ts).
447461
trace := pSpec.makeTrace(datasetSpec, {
448462
type: "milaboratories.sequence-properties",
449463
importance: 30,
450-
label: args.traceLabel,
451-
id: blockId
464+
label: args.traceLabel
452465
})
453-
stamp := func(spec) {
466+
stamp := func(spec, cHash) {
454467
return trace.inject(columnSpecs.cloneSpec(spec,
455-
{ "pl7.app/blockId": blockId },
468+
{ "pl7.app/contentHash": cHash },
456469
undefined))
457470
}
458471

459472
pf := pframes.pFrameBuilder()
473+
scalarHash := contentHash(propertiesTsv)
460474
for _, k in maps.getKeys(scalarOut) {
461475
v := scalarOut[k]
462-
pf.add(k, stamp(v.spec), v.data)
476+
pf.add(k, stamp(v.spec, scalarHash), v.data)
463477
}
464478
if aaOut != undefined {
479+
aaHash := contentHash(aaFractionTsv)
465480
for _, k in maps.getKeys(aaOut) {
466481
v := aaOut[k]
467-
pf.add(k, stamp(v.spec), v.data)
482+
pf.add(k, stamp(v.spec, aaHash), v.data)
468483
}
469484
}
470485
pf = pf.build()

0 commit comments

Comments
 (0)