From 1bc35a63d0acc98bab1a70010380b327dc9b1024 Mon Sep 17 00:00:00 2001 From: Mariia Zueva Date: Thu, 23 Apr 2026 02:17:54 +0200 Subject: [PATCH] Fix CID conflicts --- .changeset/fix-library-routing.md | 9 +++++++++ workflow/src/canonical-json.lib.tengo | 22 ++++++++++++++++++++++ workflow/src/main.tpl.tengo | 6 ++++-- workflow/src/mixcr-analyze.tpl.tengo | 9 +++------ workflow/src/mixcr-export.tpl.tengo | 7 ++----- workflow/src/process.tpl.tengo | 17 +++++++++-------- 6 files changed, 49 insertions(+), 21 deletions(-) create mode 100644 .changeset/fix-library-routing.md create mode 100644 workflow/src/canonical-json.lib.tengo diff --git a/.changeset/fix-library-routing.md b/.changeset/fix-library-routing.md new file mode 100644 index 0000000..db243a6 --- /dev/null +++ b/.changeset/fix-library-routing.md @@ -0,0 +1,9 @@ +--- +'@platforma-open/milaboratories.mixcr-amplicon-alignment.workflow': patch +--- + +Fix intermittent MiXCR failures (`Can't find library for library + custom`) and `CIDConflictError` during body re-evaluation. + +- Give `mixcr-analyze.tpl.tengo` a unique `hash_override` UUID. It was copy-pasted from `mixcr-clonotyping`, so the two templates were being deduplicated as one on the Platforma backend — silently serving the wrong bytecode. +- Route the reference library as a dedicated `extra.referenceLibrary` field instead of embedding it inside `extra.params`. Matches the mixcr-clonotyping pattern. +- Build params JSON resources with canonical (key-sorted) encoding so CIDs are stable across body re-evaluations. Tengo `json.encode` iterates Go map keys in random order, which made `stopCodonReplacements`, `aminoAcidSeqColumnPairs`, and schema maps produce different bytes each pass. \ No newline at end of file diff --git a/workflow/src/canonical-json.lib.tengo b/workflow/src/canonical-json.lib.tengo new file mode 100644 index 0000000..b2da589 --- /dev/null +++ b/workflow/src/canonical-json.lib.tengo @@ -0,0 +1,22 @@ +// Deterministic JSON resource construction for stable CIDs. +// +// The SDK's smart.createJsonResource uses json.encode, which iterates Tengo +// map keys in Go's random hash order. Under BlockModelV3 body re-evaluation +// this produces different bytes (and therefore different CIDs) for the same +// logical value, triggering CIDConflictError when pframes.processColumn +// tries to reuse a previously-assigned field slot. +// +// The SDK's :canonical module sorts keys recursively — this wrapper builds +// a json/object resource from those canonical bytes directly. + +canonical := import("@platforma-sdk/workflow-tengo:canonical") +constants := import("@platforma-sdk/workflow-tengo:constants") +smart := import("@platforma-sdk/workflow-tengo:smart") + +jsonResource := func(value) { + return smart.createValueResource(constants.RTYPE_JSON, bytes(canonical.encode(value))) +} + +export { + jsonResource: jsonResource +} diff --git a/workflow/src/main.tpl.tengo b/workflow/src/main.tpl.tengo index c263f8a..cd6f339 100644 --- a/workflow/src/main.tpl.tengo +++ b/workflow/src/main.tpl.tengo @@ -12,6 +12,8 @@ pframes := import("@platforma-sdk/workflow-tengo:pframes") exec := import("@platforma-sdk/workflow-tengo:exec") json := import("json") +canonicalJson := import(":canonical-json") + processTpl := assets.importTemplate(":process") repseqioLibraryTpl := assets.importTemplate(":repseqio-library") @@ -110,7 +112,7 @@ wf.body(func(args) { sequenceFragments: fragments, checksumVersion: 1 }] - referenceLibrary = string(json.encode(library)) + referenceLibrary = smart.createJsonResource(library) } else if mode == "libraryFile" { fImport := file.importFile(args.libraryFile) libraryImportHandle = fImport.handle @@ -133,7 +135,7 @@ wf.body(func(args) { referenceLibrary: referenceLibrary, limitInput: limitInput, - params: smart.createJsonResource(maps.clone({ + params: canonicalJson.jsonResource(maps.clone({ blockId: blockId, perProcessMemGB: perProcessMemGB, perProcessCPUs: perProcessCPUs, diff --git a/workflow/src/mixcr-analyze.tpl.tengo b/workflow/src/mixcr-analyze.tpl.tengo index de7ebf1..feb6f74 100644 --- a/workflow/src/mixcr-analyze.tpl.tengo +++ b/workflow/src/mixcr-analyze.tpl.tengo @@ -1,4 +1,4 @@ -//tengo:hash_override D70EDB25-6FF6-4615-966D-B79B04B5751C +//tengo:hash_override B6265A3B-36E7-4403-955D-9431A6BB254E // mixcr analyze @@ -26,6 +26,7 @@ self.body(func(inputs) { aggregationAxesNames := inputs[pConstants.AGGREGATION_AXES_NAMES_FIELD_NAME] params := inputs.params + referenceLibrary := inputs.referenceLibrary fileExtension := params.fileExtension limitInput := inputs.limitInput perProcessMemGB := inputs.perProcessMemGB @@ -91,11 +92,7 @@ self.body(func(inputs) { arg("--species").arg("custom"). arg("--library").arg(libraryFileName) - if is_string(params.referenceLibrary) { - mixcrCmdBuilder.writeFile(libraryFileName, params.referenceLibrary).saveFile(libraryFileName) - } else { - mixcrCmdBuilder.addFile(libraryFileName, params.referenceLibrary) - } + mixcrCmdBuilder.addFile(libraryFileName, referenceLibrary) mixcrCmdBuilder. arg("--rna"). diff --git a/workflow/src/mixcr-export.tpl.tengo b/workflow/src/mixcr-export.tpl.tengo index e4cc0a8..b1c62e8 100644 --- a/workflow/src/mixcr-export.tpl.tengo +++ b/workflow/src/mixcr-export.tpl.tengo @@ -165,6 +165,7 @@ self.body(func(inputs) { clnsFile := inputs[pConstants.VALUE_FIELD_NAME] params := inputs.params + referenceLibrary := inputs.referenceLibrary exportArgs := params.exportArgs clonotypeKeyColumns := params.clonotypeKeyColumns @@ -227,11 +228,7 @@ self.body(func(inputs) { arg("clones.tsv"). saveFile("clones.tsv") - if is_string(params.referenceLibrary) { - mixcrCmdBuilder.writeFile(libraryFileName, params.referenceLibrary).saveFile(libraryFileName) - } else { - mixcrCmdBuilder.addFile(libraryFileName, params.referenceLibrary) - } + mixcrCmdBuilder.addFile(libraryFileName, referenceLibrary) return mixcrCmdBuilder. cacheHours(3). diff --git a/workflow/src/process.tpl.tengo b/workflow/src/process.tpl.tengo index f530d7c..8265d81 100644 --- a/workflow/src/process.tpl.tengo +++ b/workflow/src/process.tpl.tengo @@ -18,6 +18,7 @@ mixcrExportTpl := assets.importTemplate(":mixcr-export") aggregateByClonotypeKeyTpl := assets.importTemplate(":aggregate-by-clonotype-key") exportReportTpl := assets.importTemplate(":export-report") calculateExportSpecs := import(":calculate-export-specs") +canonicalJson := import(":canonical-json") self.awaitState("InputsLocked") self.awaitState("params", "ResourceReady") @@ -212,9 +213,8 @@ self.body(func(inputs) { traceSteps: [{type: "milaboratories.mixcr-amplicon-alignment", id: blockId, importance: 20, label: "MiXCR generic amplicon"}], extra: { - params: maps.clone({ + params: canonicalJson.jsonResource(maps.clone({ fileExtension: fileExtension, - referenceLibrary: referenceLibrary, cloneClusteringMode: cloneClusteringMode, hasUMI: hasUMI, tagPattern: tagPattern, @@ -222,7 +222,8 @@ self.body(func(inputs) { imputeGermline: params.imputeGermline, badQualityThreshold: params.badQualityThreshold, isLibraryFileGzipped: params.isLibraryFileGzipped - }, { removeUndefs: true }), + }, { removeUndefs: true })), + referenceLibrary: referenceLibrary, limitInput: limitInput }, @@ -274,10 +275,9 @@ self.body(func(inputs) { exportOutputs, { extra: { - params: maps.clone({ + params: canonicalJson.jsonResource(maps.clone({ clonotypeKeyColumns: clonotypeKeyColumns, exportArgs: exportArgs, - referenceLibrary: referenceLibrary, mixcrChains: mixcrChains, mainIsProductiveColumn: mainIsProductiveColumn, aminoAcidSeqColumns: aminoAcidSeqColumns, @@ -286,7 +286,8 @@ self.body(func(inputs) { stopCodonTypes: params.stopCodonTypes, stopCodonReplacements: params.stopCodonReplacements, isLibraryFileGzipped: params.isLibraryFileGzipped - }, { removeUndefs: true }) + }, { removeUndefs: true })), + referenceLibrary: referenceLibrary }, metaExtra: { perProcessMemGB: perProcessMemGB @@ -334,13 +335,13 @@ self.body(func(inputs) { traceSteps: [{type: "milaboratories.mixcr-amplicon-alignment.aggregate", id: blockId + "." + chains, importance: 150, label: "Aggregate " + chains}], extra: { - params: { + params: canonicalJson.jsonResource({ mainAbundanceColumnNormalized: mainAbundanceColumnNormalized, mainAbundanceColumnUnnormalized: mainAbundanceColumnUnnormalized, schemaPerClonotypeNoAggregates: columnsToSchema(columnsSpecPerClonotypeNoAggregates), schemaPerClonotypeAggregates: columnsToSchema(columnsSpecPerClonotypeAggregates), schemaPerSample: columnsToSchema(columnsSpecPerSample) - } + }) }, metaExtra: { perProcessMemGB: perProcessMemGB