@platforma-open/milaboratories.mixcr-library-builder.workflow 1.4.0 → 2.0.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.
@@ -1,13 +1,19 @@
1
1
   WARN  Issue while reading "/home/runner/work/mixcr-library-builder/mixcr-library-builder/.npmrc". Failed to replace env in config: ${NPMJS_TOKEN}
2
2
 
3
- > @platforma-open/milaboratories.mixcr-library-builder.workflow@1.4.0 build /home/runner/work/mixcr-library-builder/mixcr-library-builder/workflow
3
+ > @platforma-open/milaboratories.mixcr-library-builder.workflow@2.0.0 build /home/runner/work/mixcr-library-builder/mixcr-library-builder/workflow
4
4
  > rm -rf dist && pl-tengo check && pl-tengo build
5
5
 
6
+ Processing "src/fasta-generation.tpl.tengo"...
7
+ Processing "src/fasta-to-table.tpl.tengo"...
6
8
  Processing "src/formatText.lib.tengo"...
9
+ Processing "src/json-merging.tpl.tengo"...
7
10
  Processing "src/main.tpl.tengo"...
8
11
  No syntax errors found.
9
12
  info: Compiling 'dist'...
10
13
  info: - writing /home/runner/work/mixcr-library-builder/mixcr-library-builder/workflow/dist/tengo/lib/formatText.lib.tengo
14
+ info: - writing /home/runner/work/mixcr-library-builder/mixcr-library-builder/workflow/dist/tengo/tpl/fasta-generation.plj.gz
15
+ info: - writing /home/runner/work/mixcr-library-builder/mixcr-library-builder/workflow/dist/tengo/tpl/fasta-to-table.plj.gz
16
+ info: - writing /home/runner/work/mixcr-library-builder/mixcr-library-builder/workflow/dist/tengo/tpl/json-merging.plj.gz
11
17
  info: - writing /home/runner/work/mixcr-library-builder/mixcr-library-builder/workflow/dist/tengo/tpl/main.plj.gz
12
18
  info: Template Pack build done.
13
19
  info: Template Pack build done.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @platforma-open/milaboratories.mixcr-library-builder.workflow
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 5a071f6: Multiple chains reference builder, updated dependecies
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [5a071f6]
12
+ - @platforma-open/milaboratories.mixcr-library-builder.software@2.0.0
13
+
3
14
  ## 1.4.0
4
15
 
5
16
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -1,3 +1,6 @@
1
1
  module.exports = { Templates: {
2
+ 'fasta-generation': { type: 'from-file', path: require.resolve('./tengo/tpl/fasta-generation.plj.gz') },
3
+ 'fasta-to-table': { type: 'from-file', path: require.resolve('./tengo/tpl/fasta-to-table.plj.gz') },
4
+ 'json-merging': { type: 'from-file', path: require.resolve('./tengo/tpl/json-merging.plj.gz') },
2
5
  'main': { type: 'from-file', path: require.resolve('./tengo/tpl/main.plj.gz') }
3
6
  }};
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  declare type TemplateFromFile = { readonly type: "from-file"; readonly path: string; };
2
- declare type TplName = "main";
2
+ declare type TplName = "fasta-generation" | "fasta-to-table" | "json-merging" | "main";
3
3
  declare const Templates: Record<TplName, TemplateFromFile>;
4
4
  export { Templates };
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { resolve } from 'node:path';
2
2
  export const Templates = {
3
+ 'fasta-generation': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/fasta-generation.plj.gz') },
4
+ 'fasta-to-table': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/fasta-to-table.plj.gz') },
5
+ 'json-merging': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/json-merging.plj.gz') },
3
6
  'main': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/main.plj.gz') }
4
7
  };
Binary file
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name": "@platforma-open/milaboratories.mixcr-library-builder.workflow",
3
- "version": "1.4.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "Block Workflow",
6
6
  "dependencies": {
7
- "@platforma-sdk/workflow-tengo": "^4.8.0"
7
+ "@platforma-sdk/workflow-tengo": "^4.9.3",
8
+ "@platforma-open/milaboratories.software-mixcr": "4.7.0-139-develop",
9
+ "@platforma-open/milaboratories.software-repseqio": "^2.5.0-13-master",
10
+ "@platforma-open/milaboratories.mixcr-library-builder.software": "2.0.0"
8
11
  },
9
12
  "devDependencies": {
10
- "@platforma-sdk/tengo-builder": "^2.1.10",
11
- "@platforma-open/milaboratories.software-mixcr": "4.7.0-139-develop"
13
+ "@platforma-sdk/tengo-builder": "^2.1.12",
14
+ "@platforma-sdk/test": "^1.39.7",
15
+ "vitest": "~2.1.8"
12
16
  },
13
17
  "scripts": {
14
18
  "build": "rm -rf dist && pl-tengo check && pl-tengo build",
@@ -0,0 +1,59 @@
1
+ exec := import("@platforma-sdk/workflow-tengo:exec")
2
+ self := import("@platforma-sdk/workflow-tengo:tpl")
3
+ ll := import("@platforma-sdk/workflow-tengo:ll")
4
+ assets := import("@platforma-sdk/workflow-tengo:assets")
5
+ file := import("@platforma-sdk/workflow-tengo:file")
6
+ text := import("text")
7
+
8
+ repseqioSw := assets.importSoftware("@platforma-open/milaboratories.software-repseqio:main")
9
+
10
+ self.defineOutputs("fasta")
11
+
12
+ self.body(func(inputs) {
13
+ libraryMap := inputs.libraryMap
14
+ config := inputs.config
15
+ coveredRegion := ""
16
+
17
+ fastaMap := {}
18
+
19
+ for name, library in libraryMap {
20
+ for chain, segments in config {
21
+ library_chain_temp := text.split(name, "_")[1]
22
+ library_chain := text.split(library_chain_temp, ".")[0]
23
+ if library_chain == chain {
24
+ for segment, segmentConfig in segments {
25
+ // Skip C segment as it doesn't need FASTA generation
26
+ if segment == "C" {
27
+ continue
28
+ }
29
+
30
+ // Check if this segment has valid configuration
31
+ if !is_undefined(segmentConfig.builtInSpecies) || !is_undefined(segmentConfig.fastaFile) {
32
+ if segment == "V" {
33
+ coveredRegion = segmentConfig.vRegionType
34
+ } else if segment == "J" {
35
+ coveredRegion = "JRegion"
36
+ } else if segment == "D" {
37
+ coveredRegion = "DRegion"
38
+ }
39
+
40
+ // Execute repseqio command only for valid segments
41
+ repseqioCmdBuilder := exec.builder().
42
+ software(repseqioSw).
43
+ arg("fasta").
44
+ arg("-g").arg(coveredRegion).
45
+ arg(name).addFile(name, library).
46
+ arg(segment + "_" + chain + ".fasta").
47
+ saveFile(segment + "_" + chain + ".fasta")
48
+ repseqioCmd := repseqioCmdBuilder.run()
49
+ fasta := repseqioCmd.getFile(segment + "_" + chain + ".fasta")
50
+ fastaMap[segment + "_" + chain + ".fasta"] = fasta
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+ return {
57
+ fasta: fastaMap
58
+ }
59
+ })
@@ -0,0 +1,95 @@
1
+ exec := import("@platforma-sdk/workflow-tengo:exec")
2
+ self := import("@platforma-sdk/workflow-tengo:tpl")
3
+ assets := import("@platforma-sdk/workflow-tengo:assets")
4
+ pframes := import("@platforma-sdk/workflow-tengo:pframes")
5
+ ll := import("@platforma-sdk/workflow-tengo:ll")
6
+ xsv := import("@platforma-sdk/workflow-tengo:pframes.xsv")
7
+
8
+
9
+
10
+ fastaToTableSw := assets.importSoftware("@platforma-open/milaboratories.mixcr-library-builder.software:fasta_to_table")
11
+
12
+ self.defineOutputs("fastaTable")
13
+
14
+ self.body(func(args) {
15
+ fastaMap := args.fasta
16
+
17
+ cmdBuilder := exec.builder().
18
+ software(fastaToTableSw)
19
+
20
+ // The python script takes an input directory. We add all fasta files to the root of the exec env.
21
+ for name, fastaFile in fastaMap {
22
+ cmdBuilder.addFile(name, fastaFile)
23
+ }
24
+
25
+ // Pass the root directory "." as the input_dir argument.
26
+ cmd := cmdBuilder.arg(".").
27
+ saveFile("combined_table.tsv").
28
+ run()
29
+
30
+ fastaTable := cmd.getFile("combined_table.tsv")
31
+
32
+ // Create column specifications for the fastaTable
33
+ fastaTableSpec := {
34
+ axes: [{
35
+ column: "chain",
36
+ spec: {
37
+ name: "pl7.app/vdj/chain",
38
+ type: "String",
39
+ annotations: {
40
+ "pl7.app/label": "Chain"
41
+ }
42
+ }
43
+ },
44
+ {
45
+ column: "segment",
46
+ spec: {
47
+ name: "pl7.app/vdj/segment",
48
+ type: "String",
49
+ annotations: {
50
+ "pl7.app/label": "Segment"
51
+ }
52
+ }
53
+ },
54
+ {
55
+ column: "gene_name",
56
+ spec: {
57
+ name: "pl7.app/vdj/gene_name",
58
+ type: "String",
59
+ annotations: {
60
+ "pl7.app/label": "Gene Name"
61
+ }
62
+ }
63
+ }],
64
+ columns: [{
65
+ column: "sequence",
66
+ spec: {
67
+ name: "pl7.app/vdj/sequence",
68
+ valueType: "String",
69
+ annotations: {
70
+ "pl7.app/label": "AA Sequence"
71
+ }
72
+ }
73
+ }
74
+ ],
75
+ storageFormat: "Binary",
76
+ partitionKeyLength: 2
77
+ }
78
+
79
+ result := xsv.importFile(
80
+ fastaTable,
81
+ "tsv",
82
+ fastaTableSpec,
83
+ { splitDataAndSpec: true }
84
+ )
85
+
86
+ pf := pframes.pFrameBuilder()
87
+ for id, v in result {
88
+ pf.add(id, v.spec, v.data)
89
+ }
90
+ pf = pf.build()
91
+
92
+ return {
93
+ fastaTable: pframes.exportFrame(pf)
94
+ }
95
+ })
@@ -0,0 +1,32 @@
1
+ exec := import("@platforma-sdk/workflow-tengo:exec")
2
+ self := import("@platforma-sdk/workflow-tengo:tpl")
3
+ ll := import("@platforma-sdk/workflow-tengo:ll")
4
+ assets := import("@platforma-sdk/workflow-tengo:assets")
5
+ file := import("@platforma-sdk/workflow-tengo:file")
6
+
7
+ mergeSw := assets.importSoftware("@platforma-open/milaboratories.mixcr-library-builder.software:main")
8
+
9
+ self.defineOutputs("library")
10
+
11
+ self.body(func(args) {
12
+ libraryMap := args.libraryMap
13
+ //fastaMap := args.fasta
14
+
15
+ mergeCmdBuilder := exec.builder().
16
+ software(mergeSw).
17
+ arg("-o").arg("merged_library.json").
18
+ saveFile("merged_library.json").
19
+ arg("-i")
20
+
21
+ for name, library in libraryMap {
22
+ mergeCmdBuilder.arg(name).
23
+ addFile(name, library)
24
+ }
25
+
26
+ mergeCmd := mergeCmdBuilder.run()
27
+ finalLibrary := mergeCmd.getFile("merged_library.json")
28
+
29
+ return {
30
+ library: finalLibrary
31
+ }
32
+ })
@@ -1,98 +1,159 @@
1
- // "hello world"
2
1
  wf := import("@platforma-sdk/workflow-tengo:workflow")
2
+ render := import("@platforma-sdk/workflow-tengo:render")
3
3
  exec := import("@platforma-sdk/workflow-tengo:exec")
4
4
  ll := import("@platforma-sdk/workflow-tengo:ll")
5
5
  assets := import("@platforma-sdk/workflow-tengo:assets")
6
6
  file := import("@platforma-sdk/workflow-tengo:file")
7
+ maps := import("@platforma-sdk/workflow-tengo:maps")
8
+ smart := import("@platforma-sdk/workflow-tengo:smart")
9
+ pcolumn := import("@platforma-sdk/workflow-tengo:pframes.pcolumn")
10
+ pframes := import("@platforma-sdk/workflow-tengo:pframes")
7
11
  times := import("times")
8
12
  json := import("json")
9
13
  rand := import("rand")
10
14
  text := import("text")
11
15
 
12
16
  mixcrSw := assets.importSoftware("@platforma-open/milaboratories.software-mixcr:main")
13
-
17
+ jsonMergingTpl := assets.importTemplate(":json-merging")
18
+ fastaGenerationTpl := assets.importTemplate(":fasta-generation")
19
+ fatsaToTableTpl := assets.importTemplate(":fasta-to-table")
14
20
  toCamelCase := import(":formatText")
15
21
 
16
- wf.body(func(args) {
22
+ chainInfos := {
23
+ "IGH": "IGHeavy",
24
+ "IGL": "IGLight",
25
+ "IGK": "IGLight",
26
+ "TRB": "TCRBeta",
27
+ "TRA": "TCRAlpha",
28
+ "TRG": "TCRGamma",
29
+ "TRD": "TCRDelta"
30
+ }
17
31
 
32
+ wf.body(func(args) {
18
33
  blockId := wf.blockId().getDataAsJson()
19
-
20
- if is_undefined(args.vFastaFile) && is_undefined(args.vSpecies) {
21
- ll.panic("expected to have either `fastaFile` or `built in`; provided no any")
22
- }
23
-
24
- if is_undefined(args.jFastaFile) && is_undefined(args.jSpecies) {
25
- ll.panic("expected to have either `fastaFile` or `built in`; provided no any")
26
- }
27
-
28
- outputs := {}
29
-
34
+ chainConfigs := args.chainConfigs
30
35
  species := args.species
31
36
  cmdSpecies := toCamelCase.toCamelCase(species) + "_custom"
32
- chain := args.chain
33
-
34
- libraryBuilderCmdBuilder := undefined
35
-
36
37
  taxonId := string(rand.intn(9000) + 1000)
37
- libraryBuilderCmdBuilder = exec.builder().
38
- software(mixcrSw).
39
- mem("8GiB").
40
- cpu(1).
41
- secret("MI_LICENSE", "MI_LICENSE").
42
- arg("buildLibrary").
43
- arg("--species").arg(cmdSpecies).
44
- arg("--chain").arg(chain).
45
- arg("--taxon-id").arg(taxonId)
46
- if !is_undefined(args.vFastaFile) {
47
- vImport := file.importFile(args.vFastaFile)
48
- outputs.vImportHandle = vImport.handle
49
- libraryBuilderCmdBuilder.arg("--v-gene-feature").arg("VRegion").
50
- arg("--v-genes-from-fasta").arg("v.fasta").
51
- addFile("v.fasta", vImport.file)
52
- } else {
53
- libraryBuilderCmdBuilder.arg("--v-genes-from-species").arg(args.vSpecies)
38
+ outputs := {}
39
+ fileImports := {}
40
+ libraryMap := {}
41
+ debugOutputMap := {}
42
+ fileImportsProgress := {}
43
+ chains := []
44
+ fivePrimePrimer := args.fivePrimePrimer
45
+
46
+ for chain, chainConfig in chainConfigs {
47
+ libraryBuilderCmdBuilder := exec.builder().
48
+ software(mixcrSw).
49
+ secret("MI_LICENSE", "MI_LICENSE").
50
+ arg("buildLibrary").
51
+ arg("--species").arg(cmdSpecies).
52
+ arg("--chain").arg(chain).
53
+ arg("--taxon-id").arg(taxonId).
54
+ arg("--debug")
55
+
56
+ if chainConfig["V"].sourceType == "built-in" {
57
+ libraryBuilderCmdBuilder.arg("--v-genes-from-species").arg(chainConfig["V"].builtInSpecies)
58
+ } else if chainConfig["V"].sourceType == "fasta" {
59
+ vImport := file.importFile(chainConfig["V"].fastaFile)
60
+ fileImports[chainConfig["V"].fastaFile] = vImport
61
+ fileImportsProgress[chainConfig["V"].fastaFile] = vImport.handle
62
+ libraryBuilderCmdBuilder.arg("--v-gene-feature").arg("VRegion").
63
+ arg("--v-genes-from-fasta").arg("v.fasta").
64
+ addFile("v.fasta", vImport.file)
65
+ }
66
+
67
+ if chainConfig["J"].sourceType == "built-in" {
68
+ libraryBuilderCmdBuilder.arg("--j-genes-from-species").arg(chainConfig["J"].builtInSpecies)
69
+ } else if chainConfig["J"].sourceType == "fasta" {
70
+ jImport := file.importFile(chainConfig["J"].fastaFile)
71
+ fileImports[chainConfig["J"].fastaFile] = jImport
72
+ fileImportsProgress[chainConfig["J"].fastaFile] = jImport.handle
73
+ libraryBuilderCmdBuilder.arg("--j-genes-from-fasta").arg("j.fasta").
74
+ addFile("j.fasta", jImport.file)
75
+ }
76
+
77
+ if chainConfig["D"].builtInSpecies != undefined || chainConfig["D"].fastaFile != undefined {
78
+ if chainConfig["D"].sourceType == "built-in" {
79
+ libraryBuilderCmdBuilder.arg("--d-genes-from-species").arg(chainConfig["D"].builtInSpecies)
80
+ } else if chainConfig["D"].sourceType == "fasta" {
81
+ dImport := file.importFile(chainConfig["D"].fastaFile)
82
+ fileImports[chainConfig["D"].fastaFile] = dImport
83
+ fileImportsProgress[chainConfig["D"].fastaFile] = dImport.handle
84
+ libraryBuilderCmdBuilder.arg("--d-genes-from-fasta").arg("d.fasta").
85
+ addFile("d.fasta", dImport.file)
86
+ }
87
+ }
88
+
89
+ if chainConfig["C"].builtInSpecies != undefined || chainConfig["C"].fastaFile != undefined {
90
+ if chainConfig["C"].sourceType == "built-in" {
91
+ libraryBuilderCmdBuilder.arg("--c-genes-from-species").arg(chainConfig["C"].builtInSpecies)
92
+ } else if chainConfig["C"].sourceType == "fasta" {
93
+ cImport := file.importFile(chainConfig["C"].fastaFile)
94
+ fileImports[chainConfig["C"].fastaFile] = cImport
95
+ fileImportsProgress[chainConfig["C"].fastaFile] = cImport.handle
96
+ libraryBuilderCmdBuilder.arg("--c-genes-from-fasta").arg("c.fasta").
97
+ addFile("c.fasta", cImport.file)
98
+ }
99
+ }
100
+
101
+ libraryBuilderCmdBuilder.arg("library_" + chain + ".json").
102
+ saveFile("library_" + chain + ".json").
103
+ printErrStreamToStdout()
104
+ libraryBuilderCmd := libraryBuilderCmdBuilder.run()
105
+
106
+ debugOutput := libraryBuilderCmd.getStdoutStream()
107
+ library := libraryBuilderCmd.getFile("library_" + chain + ".json")
108
+ libraryMap["library_" + chain + ".json"] = library
109
+ debugOutputMap[chain] = debugOutput
110
+ chains = append(chains, chainInfos[chain])
54
111
  }
55
112
 
56
- if !is_undefined(args.jFastaFile) {
57
- jImport := file.importFile(args.jFastaFile)
58
- outputs.jImportHandle = jImport.handle
59
- libraryBuilderCmdBuilder.arg("--j-genes-from-fasta").arg("j.fasta").
60
- addFile("j.fasta", jImport.file)
61
- } else {
62
- libraryBuilderCmdBuilder.arg("--j-genes-from-species").arg(args.jSpecies)
113
+ seen := {}
114
+ chains_unique := []
115
+ for chain in chains {
116
+ if !seen[chain] {
117
+ seen[chain] = true
118
+ chains_unique = append(chains_unique, chain)
119
+ }
63
120
  }
64
121
 
65
- if !is_undefined(args.dFastaFile) {
66
- dImport := file.importFile(args.dFastaFile)
67
- outputs.dImportHandle = dImport.handle
68
- libraryBuilderCmdBuilder.arg("--d-genes-from-fasta").arg("d.fasta").
69
- addFile("d.fasta", dImport.file)
70
- } else if !is_undefined(args.dSpecies) {
71
- libraryBuilderCmdBuilder.arg("--d-genes-from-species").arg(args.dSpecies)
122
+ // Create logs map using pcolumn.resourceMapBuilder
123
+ debugOutputMapBuilder := pcolumn.resourceMapBuilder(/* keyLength */ 1)
124
+ for chain, debugOutput in debugOutputMap {
125
+ debugOutputMapBuilder.add([chain], debugOutput)
72
126
  }
73
127
 
74
- if !is_undefined(args.cFastaFile) {
75
- cImport := file.importFile(args.cFastaFile)
76
- outputs.cImportHandle = cImport.handle
77
- libraryBuilderCmdBuilder.arg("--c-genes-from-fasta").arg("c.fasta").
78
- addFile("c.fasta", cImport.file)
79
- } else if !is_undefined(args.cSpecies) {
80
- libraryBuilderCmdBuilder.arg("--c-genes-from-species").arg(args.cSpecies)
81
- }
128
+ outputs.debugOutput = debugOutputMapBuilder.build()
129
+
130
+ outputs.fileImports = smart.createMapResource(maps.mapValues(fileImportsProgress, func(handle) {
131
+ return handle
132
+ }))
133
+
134
+ fastaGeneration := render.create(fastaGenerationTpl, {
135
+ libraryMap: libraryMap,
136
+ config: chainConfigs
137
+ })
138
+ fasta := fastaGeneration.output("fasta", 24 * 60 * 60 * 1000)
82
139
 
83
- libraryBuilderCmdBuilder.arg("library.json").
84
- saveFile("library.json").
85
- printErrStreamToStdout()
86
- libraryBuilderCmd := libraryBuilderCmdBuilder.run()
140
+ fastaToTable := render.create(fatsaToTableTpl, {
141
+ fasta: fasta
142
+ })
143
+ fastaTable := fastaToTable.output("fastaTable", 24 * 60 * 60 * 1000)
87
144
 
88
- debugOutput := libraryBuilderCmd.getStdoutStream()
89
- library := libraryBuilderCmd.getFile("library.json")
145
+ mergeResults := render.create(jsonMergingTpl, {
146
+ libraryMap: libraryMap,
147
+ fastaTable: fastaTable
148
+ })
90
149
 
91
- outputs.debugOutput = debugOutput
150
+ finalLibrary := mergeResults.output("library", 24 * 60 * 60 * 1000)
151
+
152
+ outputs.fastaTable = fastaTable
92
153
 
93
154
  exports := {
94
- library : {
95
- data: library,
155
+ library: {
156
+ data: finalLibrary,
96
157
  spec: {
97
158
  kind: "File",
98
159
  name: "pl7.app/vdj/library",
@@ -101,15 +162,15 @@ wf.body(func(args) {
101
162
  },
102
163
  annotations: {
103
164
  "pl7.app/species": cmdSpecies,
104
- "pl7.app/vdj/chain": chain,
165
+ "pl7.app/vdj/chain": string(chains),
105
166
  "pl7.app/vdj/isLibrary": "true",
106
167
  "pl7.app/vdj/libraryFormat": "repseqio.json",
107
- "pl7.app/label": text.split(species, "_custom")[0] + " " + chain + " library"
168
+ "pl7.app/label": text.split(species, "_custom")[0] + " library"
108
169
  }
109
170
  }
110
- }
171
+ }
111
172
  }
112
-
173
+
113
174
  return {
114
175
  outputs: outputs,
115
176
  exports: exports