Structured contract copied for validation or lookup.
- Purpose
- Validate the emitted Nextflow summary JSON and provide downstream consumers the output contract.
Read a Nextflow pipeline source tree (nf-core or ad-hoc DSL2) and emit a structured JSON summary for downstream translation Molds.
Every file beside index.md is one the mold kind declares.
source-specific fields are coherent.
Abstract oracle: the properties any cast of this Mold must satisfy.
eval.md declares properties and check type.
eval.md ↗Concrete cases bound to fixtures, run against the eval properties.
scenarios.md declares cases bound to fixtures.
scenarios.md ↗9 typed references; 0 resolver issues.
All on-demand references describe triggers.
Hypothesis references include verification.
Typed references describe what casting consumes, and when the generated skill should load each artifact.
Structured contract copied for validation or lookup.
Structured contract copied for validation or lookup.
Structured contract copied for validation or lookup.
Structured contract copied for validation or lookup.
Tool-level install metadata. Aggregated into the cast bundle's Required Tools section.
Background synthesis loaded by explicit progressive-disclosure metadata.
Background synthesis loaded by explicit progressive-disclosure metadata.
Background synthesis loaded by explicit progressive-disclosure metadata.
Background synthesis loaded by explicit progressive-disclosure metadata.
A structured JSON summary of a Nextflow pipeline, including its interface, processes, data flow, software environment, and test fixtures.
Read a Nextflow pipeline source tree (nf-core or ad-hoc DSL2) and emit a structured JSON summary describing its processes, channels, conditionals, containers, parameters, and test fixtures. Source-specific (Nextflow), target-agnostic. The summary is the input to every downstream Mold in the NEXTFLOW → GALAXY and NEXTFLOW → CWL pipelines: nextflow-summary-to-galaxy-interface, nextflow-summary-to-galaxy-data-flow, nextflow-summary-to-cwl-interface, nextflow-summary-to-cwl-data-flow, author-galaxy-tool-wrapper (for the container/conda block), nextflow-test-to-galaxy-test-plan, and nextflow-test-to-cwl-test-plan (for the test-fixture block).
This Mold owns only the read-and-structure step. Every cross-source-and-target translation lives downstream; this Mold is responsible for surfacing what exists in the NF tree honestly, not for reshaping it toward Galaxy or CWL idioms.
The output schema is per-source by design — see gxy-sketches-alignment for why a forced-shared cross-source summary shape was rejected.
The Mold expects:
SketchSource semantics from gxy-sketches.test, test_full, …) applied while resolving each candidate’s effective profile chain. It does not identify a test case. When omitted, there is no silent profile default.inputs[].path.Whole-pipeline only. The Mold does not accept “summarize this single subworkflow” subset hints; subset summarization is an open question — see Non-goals.
A single JSON document conforming to summary-nextflow (packages/summarize-nextflow/src/schema/summary-nextflow.schema.json). Sketch shape:
{
"source": { // mirrors SketchSource
"ecosystem": "nf-core" | "nextflow",
"workflow": "rnaseq",
"url": "https://github.com/nf-core/rnaseq",
"version": "3.14.0", // tag or commit SHA
"license": "MIT",
"slug": "nf-core-rnaseq"
},
"params": [
{ "name": "input", "type": "path", "default": null,
"description": "Samplesheet CSV", "required": true }
],
"sample_sheets": [
{ "param": "input",
"schema_path": "assets/schema_input.json",
"discovered_via": "nf-schema",
"format": "csv", "header": true,
"columns": [
{ "name": "sample", "type": "string", "kind": "meta", "required": true,
"pattern": "^\\S+$" },
{ "name": "fastq_1", "type": "string", "kind": "data", "format": "file-path",
"required": true, "exists": true, "pattern": "^\\S+\\.f(ast)?q\\.gz$" },
{ "name": "fastq_2", "type": "string", "kind": "data", "format": "file-path",
"required": false, "exists": true, "pattern": "^\\S+\\.f(ast)?q\\.gz$" },
{ "name": "strandedness","type": "string", "kind": "meta", "required": true,
"enum": ["forward", "reverse", "unstranded", "auto"] }
] }
],
"profiles": [ // classified from the body, never the name
{ "name": "test", "kinds": ["test"], "source_path": "nextflow.config",
"includes": ["conf/test.config"],
"signals": ["include:test-config", "sets-input-data", "params-assignment"] },
{ "name": "test_full", "kinds": ["test"], "source_path": "nextflow.config",
"includes": ["conf/test_full.config"],
"signals": ["include:test-config", "sets-input-data", "params-assignment"] },
{ "name": "docker", "kinds": ["container"], "source_path": "nextflow.config",
"includes": [], "signals": ["container-keys"] },
{ "name": "debug", "kinds": ["dev"], "source_path": "nextflow.config",
"includes": [], "signals": ["dev-profile-name"] }
],
"tools": [ // mirrors gxy-sketches ToolSpec, augmented
{ "name": "fastp", "version": "0.23.4",
"biocontainer": "biocontainers/fastp:0.23.4--h5f740d0_0", // accepts quay.io/ or docker.io biocontainers/ alias
"bioconda": "bioconda::fastp=0.23.4",
"docker": null,
"singularity": "https://depot.galaxyproject.org/singularity/fastp:0.23.4--h5f740d0_0",
"wave": null } // Seqera Wave / community-cr registry
],
"processes": [
{ "name": "MINIMAP2_ALIGN", // canonical name
"aliases": ["MINIMAP2_CONSENSUS", "MINIMAP2_POLISH"], // re-imported under multiple names; edges reference the alias
"module_path": "modules/nf-core/minimap2/align/main.nf",
"tool": "minimap2_mulled", // FK into tools[].name
"container": "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? '<sing-uri>' : '<other-uri>' }", // verbatim directive
"conda": "${moduleDir}/environment.yml", // verbatim directive
"inputs": [ { "name": "reads", "shape": "tuple(val(meta), path(reads))", "description": "...", "topic": null } ],
"outputs": [ { "name": "paf", "shape": "tuple(val(meta), path(\"*.paf\")) optional", "description": "...", "topic": null },
{ "name": "versions", "shape": "path(\"versions.yml\")", "description": "tool versions YAML", "topic": null } ],
"when": null,
"script_excerpt": "\"\"\"\n$args\nminimap2 -t $task.cpus $reference $reads > ${prefix}.paf\n\"\"\"", // verbatim script body; script_summary is the LLM pass's job
"publish_dir": null }
],
"subworkflows": [
{ "name": "FASTQ_TRIM_FASTP_FASTQC",
"path": "subworkflows/nf-core/fastq_trim_fastp_fastqc/main.nf",
"kind": "pipeline",
"aliases": [],
"calls": ["FASTP", "FASTQC_RAW", "FASTQC_TRIM"],
"inputs": [], "outputs": [] },
{ "name": "PIPELINE_INITIALISATION",
"path": "subworkflows/local/utils_nfcore_<name>_pipeline/main.nf",
"kind": "utility", // composes free functions, no process invocations
"aliases": [],
"calls": [],
"inputs": [], "outputs": [
{ "name": "samplesheet", "shape": "tuple(meta, path)", "description": "validated --input", "topic": null }
] }
],
"workflow": {
"name": "RNASEQ",
"channels": [
{ "name": "ch_samplesheet",
"source": "Channel.fromList(samplesheetToList(params.input, '...'))",
"shape": "tuple(meta, [path,path])",
"construct": "samplesheetToList",
"from_param": "input",
"required_runtime": false }
],
"edges": [
{ "from": "ch_samplesheet", "to": "FASTP", "via": [] },
{ "from": "FASTP.out.reads", "to": "STAR_ALIGN",
"via": ["map", "join"] }
],
"conditionals": [
{ "guard": "params.skip_alignment", "branch": "alternate",
"affects": ["STAR_ALIGN"] }
]
},
"test_candidates": [
{ "id": "tests/dfast.nf.test::-profile test_dfast",
"kind": "nf-test",
"name": "-profile test_dfast",
"path": "tests/dfast.nf.test",
"effective_profiles": ["test_dfast"],
"params_delta": { "outdir": "$outputDir" },
"inputs": [ /* TestDataRef-shaped */ ],
"outputs": [ /* ExpectedOutputRef-shaped */ ],
"execution_mode": "real",
"scope": "primary",
"disposition": "eligible",
"rationale": "whole-pipeline nf-test case from tests/dfast.nf.test",
"assert_workflow_success": true,
"snapshot": {
"captures": ["succeeded_task_count", "versions_yml", "stable_names", "stable_paths"],
"helpers": ["getAllFilesFromDir", "removeNextflowVersion"],
"ignore_files": ["tests/.nftignore", "tests/.nftignore_files_entirely"],
"ignore_globs": [],
"snap_path": "tests/dfast.nf.test.snap"
},
"prose_assertions": [] }
],
"test_selection": {
"status": "needs-scope-choice",
"selected_candidate_id": null,
"rationale": "multiple primary-coverage cases remain incomparable"
}
}
Field-name parity with gxy-sketches (SketchSource, ToolSpec, TestDataRef, ExpectedOutputRef) is intentional and load-bearing — see gxy-sketches-alignment §1-3.
The cast skill is not a single LLM prompt over the source tree. It is a small program with one or two embedded LLM calls. The split is:
nextflow.config and nextflow_schema.json, regex-tokenize process blocks for typed fields (name, container, conda, declared IO channel names, when: guards, publishDir), read nf-core module meta.yml verbatim, enumerate include { X } from '...' for the call graph, resolve biocontainer image strings.script_summary of each process, over the verbatim script_excerpt the deterministic pass carries; reconciliation of operator-chained channel paths (A | map | join(B) | groupTuple) into the workflow edges[], free-text description / notes fields, IO inference when meta.yml is absent and the script is the only signal.Everything the schema demands as a typed enum or path is deterministic. Free-text fields are LLM. The schema enforces that boundary by typing.
Branch shallow on layout:
nextflow.config declares manifest.name = 'nf-core/...'; modules/nf-core/, subworkflows/nf-core/, and nextflow_schema.json are present. Prefer meta.yml as IO ground truth.nextflow_schema.json, no module meta.yml. Falls back to script:-block IO inference. Consult component-nextflow-pipeline-anatomy when layout differs from nf-core conventions in ways these rules do not cover.source block and exit early with a warnings[] entry. Out of scope for v1.Real pipelines have multiple named workflow blocks — typically an anonymous workflow {} entrypoint in main.nf that wires PIPELINE_INITIALISATION → NFCORE_<NAME> → PIPELINE_COMPLETION, plus a substantive named workflow under workflows/<name>.nf. Selection rule for the primary workflow: pick the named workflow that invokes the most pipeline processes. The anonymous workflow {} glue and the NFCORE_<NAME> wrapper land in subworkflows[], marked kind: utility and kind: pipeline respectively.
Populate source from git remote get-url, git rev-parse HEAD (or the user-supplied pin), manifest.name / manifest.homePage / manifest.version in nextflow.config, and LICENSE filename detection. slug is kebab of <owner>-<repo> for nf-core, kebab of repo basename otherwise.
Read nextflow.config params { ... } block for defaults. When nextflow_schema.json exists (nf-core), prefer it as the source of truth for type, description, and required — it is real JSON Schema, copy verbatim. Some params are computed at config-load time (for example params.fasta = getGenomeAttribute('fasta') in main.nf) and will not appear in nextflow_schema.json; include them with a description noting the dynamic source.
profiles[] — enumerate the profiles { ... } block and classify each entry by role. Track brace depth: a profile body contains nested params { } and process { } blocks, and a flat regex over the block emits those as profile names. The block is not always in nextflow.config — follow the root includeConfig chain (epi2me-labs/wf-human-variation declares all six profiles in base.config), and fall back to a bounded scan for other *.config files carrying a profiles { } block (biocorecrg/MOP2 keeps them in nextflow.global.config, selected at launch with -c).
Classify from the profile body, resolving its includeConfig targets first — never from the name. The name heuristic fails both ways: replikation/What_the_Phage’s test profile only flips fasta = true, a mode switch with no test data, while nf-core/sarek’s mutect profile includes conf/test_mutect2.config and carries explicit test-data evidence. Profiles that pair a container engine with an executor are common outside nf-core — seven of biocorecrg/MOP2’s eight do, and CRG-CNAG/CalliNGS-NF’s cluster sets process.container, singularity.enabled, and executor = 'crg' in one block — so record every role, not just the first.
kinds entry | evidence in the profile body |
|---|---|
test | includes a conf/test*.config, or assigns a non-null params.input / samplesheet |
container | docker.enabled, conda.enabled, process.container, process.conda, process.arch, … |
executor | executor.*, workDir, executor / queue / clusterOptions inside process { }, an aws {} / google {} / k8s {} block |
resources | process directives only — memory, cpus, time, withLabel: selectors |
mode | params that change behavior without supplying input data (nf-core/rnaseq’s prokaryotic) |
dev | debug, gitpod |
unknown | no classifying signal |
test, container, executor, and dev accumulate. mode and resources are assigned only when none of those matched — their signals are much weaker, since nf-core’s docker profile assigns params.use_gpu inside a ternary and most test profiles set process resource caps, so accumulating them would label most of a pipeline mode. Record the suppressed evidence in signals[] instead.
kinds: ["test"] is profile-level candidate evidence, not the test-selection decision. When pipeline-level nf-test-derived test_candidates[] entries exist, those test cases are the higher-fidelity unit because they also carry per-test parameter overrides; only fall back to an input-bearing profile when no pipeline-level nf-test case exists. The kind does not establish cost or representative coverage. test_full remains input-bearing but is commonly realistic-scale, while test_minimal / test_tiny may disable the science stages that the target translation must cover. Mode profiles can likewise encode scope choices that a target translation needs to surface; only execution-environment roles such as container and executor are intrinsically irrelevant to target workflow semantics.
Two limits worth stating plainly rather than leaving for a consumer to discover. test detection keys on nf-core input conventions, so an ad-hoc pipeline naming its inputs reads / genome gets no test profile even where one exists in spirit — across the fixture corpus all 103 test profiles are in the 16 nf-core pipelines and none in the 10 ad-hoc ones. And a profiles { } block outside the detected pipeline root is not found at all: ncbi/egapx keeps one in ui/assets/config/user/, which is launcher-side config for its Python wrapper rather than pipeline profiles.
test_full is kinds: ["test"] like any other; includes exposes the config family as a ranking signal rather than making it a separate kind. Populate signals[] with the evidence acted on so a surprising classification is auditable, and source_path with the config file that declared the profile.
Sample-sheet inputs are the dominant structured-input idiom in modern nf-core pipelines and the most lossy thing to leave as prose inside params[].description. For each candidate sample-sheet parameter, populate one sample_sheets[] entry capturing the row schema deterministically. Discovery has three branches, recorded in discovered_via:
nf-schema: the param’s nextflow_schema.json entry has a schema: keyword pointing at a sibling JSON Schema file (assets/schema_*.json). Read that file. Each property in the row schema maps to one SampleSheetColumn. Preserve property order, not source-column order — samplesheetToList() emits columns in property order, and downstream channel item layout depends on it.samplesheetToList: the workflow imports samplesheetToList from nf-schema and calls it on the param. When the call cites a schema path, follow it. Without a schema path, emit the entry with schema_path: null and infer columns from splitCsv-shaped fallback if any; otherwise emit columns: [] and a warnings[] note.splitCsv: a Channel.fromPath(params.X).splitCsv(header: true) materialization. Header inference only — emit columns by name, leave type: string, kind inferred from downstream path() consumption when traceable, else meta. Mark discovered_via: splitCsv.ad-hoc: pipeline-specific CSV/TSV parsing detected from script bodies (e.g. row-zero/row-one indexing). Emit a minimal entry with columns: [] plus a warnings[] advisory; downstream Molds will need to handle these by hand.Column field rules:
kind: data when nf-schema format is file-path/directory-path/path or when the column is annotated meta: is absent and the value is consumed as a path() downstream. meta otherwise (including all meta: true annotations and all non-path scalars). Nest the nf-schema meta: annotation here even when implicit — translation Molds key on it to decide which columns become Galaxy column_definitions[] versus element/inner-collection slots.type: copy verbatim from the row schema (string/integer/number/boolean). Path columns are string with a format qualifier; do not collapse path into a synthetic type.required, default, enum, pattern, exists, mimetype, description: copy verbatim when present, leaving null/empty defaults otherwise.This step does not reshape onto any target idiom (Galaxy sample_sheet:paired vs list:paired is not decided here). It records what the source pipeline declares; the variant choice belongs to nextflow-summary-to-galaxy-interface and nextflow-summary-to-cwl-interface.
For each process <NAME> { ... } in main.nf, workflows/, modules/**, subworkflows/**:
container, conda, publishDir, when: directives verbatim into processes[].container / processes[].conda. Modern nf-core directives are ternary expressions (workflow.containerEngine == 'singularity' ? <sing-uri> : <docker-uri>) and file references (${moduleDir}/environment.yml); keep the directive text intact and resolve into tools[] separately (§5).input: and output: blocks for declared channel names and shapes — typed channels (tuple val(meta), path(reads)) become shape strings ("tuple(meta, [path])"); arity is preserved as a string, not structured.include { ... } statements across the pipeline (main.nf, workflows/, subworkflows/**) to populate both processes[].aliases and subworkflows[].aliases. include { MINIMAP2_ALIGN as MINIMAP2_CONSENSUS } adds MINIMAP2_CONSENSUS to the canonical process; the same rule maps aliased subworkflow calls back to their canonical declaration. Calls and edges retain the alias spelling used by the source. Ignore includes inside line or block comments.topic: <name> annotations on outputs (Nextflow 24+ channel topics — nf-core templates emit tuple(val("${task.process}"), val('toolname'), eval(...)) topic: versions for version aggregation). Record the topic name in ChannelIO.topic.meta.yml exists, use it for description and IO documentation rather than parsing the script: block.script:/shell:/exec: body verbatim into script_excerpt (dedented, truncated at 4000 characters, stub: dropped). For ad-hoc pipelines with no meta.yml this is the only evidence of what a process does.script_excerpt in one line into script_summary. Ask only for what the tool does. Deterministic runs omit script_summary rather than emitting a placeholder.Walk per-process container and conda directives. Container directives are usually ternary — extract both branches:
singularity ? branch typically yields an https://depot.galaxyproject.org/singularity/<name>:<version>--<build> URL → tools[].singularity.quay.io/biocontainers/<name>:<version>--<build> → tools[].biocontainer.biocontainers/<name>:<version>--<build> (docker.io alias for the same biocontainer image) → tools[].biocontainer (same field; both forms are biocontainer images).community.wave.seqera.io/library/<name>:<version>--<digest> or https://community-cr-prod.seqera.io/.../sha256/<digest>/data → tools[].wave.tools[].docker.Conda directives are usually file references to ${moduleDir}/environment.yml; read the file and extract its dependencies: list. Each bioconda::<name>=<version> entry becomes a tools[] entry with tools[].bioconda set to the original dependency string. Multi-tool environments are common (minimap2 + samtools + htslib, racon + multiqc); keep every Bioconda dependency rather than selecting the first. Legacy literal-string directives (conda "bioconda::<name>=<version>") feed the same field, as does the pre-environment.yml ternary form conda (params.enable_conda ? "<spec>" : null).
Read the version off each spec rather than assuming one shape: name=1.0 and name==1.0 both pin exactly, a trailing =<build> is a conda build string and not part of the version, and an inexact constraint (name>=1.17) goes to tools[].version_constraint verbatim with version left unknown. A spec naming no version is legitimately unpinned — unknown with no constraint. Never let unknown into tools[].versions[]; it is the absence of a version, not a version. A spec you cannot read must land in warnings[] — the eval property is that every directive resolves or is flagged, and a directive that resolves in part satisfies neither unless the unread spec is named.
Tool name and version are typically derivable from any of the resolved fields. Deduplicate by (name, version) across processes; one entry per tool. processes[].tool is a foreign key into tools[].name; leave it null when a process declares several packages and none is “the” tool (bioconda::xopen=1.1.0 bioconda::pysam=0.16.0) rather than picking one arbitrarily. This block is the bridge to author-galaxy-tool-wrapper — it consumes container/conda info to choose or justify the UDT container.
Enumerate the top-level workflow’s include statements and channel construction (Channel.fromPath, Channel.fromFilePairs, Channel.fromList(samplesheetToList(...)), splitCsv, file()/files(), params.*, channel.empty(), channel.topic('<name>')). For operator chains, the deterministic parser records the literal chain (["map", "join", "groupTuple"] in via). Reconciling chained operators into a coherent from → to edge is the second LLM call: given the literal chain, the source channel shape, and the downstream process’s declared input shape, emit the resolved edge.
For each emitted workflow.channels[] entry, populate three classified fields alongside the verbatim source:
construct — typed enum reflecting the channel’s primary materialization factory or shape-determining operator. Selection precedence: (1) samplesheetToList when the chain contains samplesheetToList(...); (2) splitCsv when the chain ends in .splitCsv(header: true) over a path; (3) otherwise the outermost factory (Channel.fromPath → fromPath, Channel.fromFilePairs → fromFilePairs, Channel.fromList → fromList, file(...) → file, files(...) → files, Channel.of → of, Channel.value → value, Channel.empty → empty, Channel.topic → topic); (4) other for derived/operator-only constructions.from_param — FK into params[].name when the construction expression directly references params.X (e.g. Channel.fromPath(params.reads), samplesheetToList(params.input, ...), file(params.fasta)). v1 is direct-only — one-hop Groovy bindings (def reads = params.reads; Channel.fromPath(reads)) are deferred to galaxyproject/foundry#211. Null when no direct reference, or when construct is not data-bearing (empty, of, value, topic, other).required_runtime — true when the construction chain ends in .ifEmpty { error ... } (or an equivalent imperative emptiness-throw guard). Captures runtime requiredness even when the param’s nf-schema entry does not mark it required. False otherwise.All three fields are syntactic: regex-level extraction over the construction expression, no LLM call.
Workflow-level conditionals (if (params.skip_alignment) { ... }) emit conditionals[] entries with the guard, the branch (alternate vs default), and the set of processes affected.
Subworkflows split into two kinds:
kind: pipeline — invokes pipeline processes (data-flow contributor). The NFCORE_<NAME> wrapper and any nested subworkflows/local/ that calls processes.kind: utility — composes free-function calls only (paramsHelp, samplesheetToList, completionEmail, imNotification). nf-core template subworkflows like PIPELINE_INITIALISATION and PIPELINE_COMPLETION. Subworkflow.calls is empty for utilities; their job is to produce channels (e.g. the validated samplesheet) the primary workflow consumes.Parse only live workflow declarations. Definitions, includes, processes, and calls inside // ... or /* ... */ comments are inert source text and must never contribute to the summary or overwrite a live declaration with the same name. Preserve each live subworkflow’s take:, emit:, and distinct alias call spellings; populate subworkflows[].aliases on the canonical declaration.
Free-function calls in the workflow body itself (paramsSummaryMap, softwareVersionsToYAML, methodsDescriptionText) are not modeled as processes or subworkflows. Their channel outputs flow into the primary workflow’s channels[]; the function names are nf-core template idiom, not pipeline-specific signal. Operator chains with deeply nested closures may produce edges flagged with low confidence in notes.
Emit one test_candidates[] list and one test_selection result. Do not emit a parallel singular fixture record or a second top-level nf-test list.
Apply nextflow-test-case-selection in fidelity order:
test(...) block inside a live pipeline-level nextflow_pipeline {} suite under tests/. Resolve effective_profiles[] from the root nf-test.config directive, file/test-level directives, then the optional caller override. Replacement and +name extension semantics matter; the descriptive test name is never configuration evidence. When Groovy generates cases dynamically, preserve one dynamically-generated aggregate candidate for the file and emit a warning.profiles[] entry whose body-derived kinds contains test; an explicit caller profile is a candidate even when its body could not be classified.pipeline-defaults candidate from the selected entrypoint’s declared defaults. Select it only if required launch parameters resolve.For every candidate, populate its stable id, source kind, path, resolved profiles, params_delta, inputs, outputs, execution mode, scope, disposition, rationale, and assertion fields. Read each effective profile’s static includeConfig targets and conventional conf/<profile>.config; merge per-test when { params { ... } } overrides last. Extract remote URLs and repo-relative paths into that candidate’s own inputs[].
When fixture fetching is enabled, hash each fetched remote file with SHA-1. When a test-data directory is provided, write the samplesheet and every referenced remote file under that directory using a deterministic URL-derived path and record that local filesystem path in path while preserving the original url. Fetch and expand samplesheets per candidate so alternatives do not share or overwrite one singular fixture list.
Each input/output entry follows TestDataRef / ExpectedOutputRef field names verbatim. The path vs url rules from gxy-sketches’ TestDataRef carry over, with one extension: path may be the local fetched path for a remote URL. The “must be under test_data/” constraint does not — see gxy-sketches-alignment §1.
For nf-test-derived candidates:
name = the description string passed to test("...").path = repo-relative file path.effective_profiles[] = resolved nf-test/profile chain, never text scraped from name.params_delta = profile changes plus the when { params { ... } } block.execution_mode = real, stub, mixed, or unknown from the case options.assert_workflow_success = true when an assert workflow.success (or equivalent) clause is present.snapshot = structured SnapshotFixture when an assert snapshot(...).match() clause is present, else null. nf-core templates use a near-uniform snapshot pattern; extract:
captures[] = logical names of values passed into snapshot(...) (typical set: succeeded_task_count, versions_yml, stable_names, stable_paths).helpers[] = nf-test helper functions invoked (getAllFilesFromDir, removeNextflowVersion, …).ignore_files[] = repo-relative paths passed as ignoreFile: to helpers (e.g. tests/.nftignore).ignore_globs[] = inline ignore: [...] glob list from helpers.snap_path = repo-relative path of the corresponding .nf.test.snap file.prose_assertions[] = any other complex/non-snapshot assertions, summarized to prose strings. Empty for snapshot-only tests (the common nf-core case).Classify full-scale candidates as reference-scale and minimal/tiny/stub-only candidates as bootstrap-only; neither wins by name alone. Select exactly one primary candidate in default.nf.test or main.nf.test, otherwise the sole primary candidate. For profile fallbacks, prefer a body-classified primary profile named test, otherwise the sole primary profile. When several primary candidates remain, set test_selection.status: needs-scope-choice, keep every candidate, and select none.
Consult component-nextflow-testing when fixtures use a layout outside conf/test.config + nf-test (e.g. legacy test/ scripts, external test harnesses) or when assertions are non-snapshot equality / regex / containsString checks.
Validate the assembled object before emitting: run foundry validate-summary-nextflow summary-nextflow.json. The subcommand is shipped by @galaxy-foundry/gxwf-foundry and can be invoked from npm with npx --package @galaxy-foundry/gxwf-foundry foundry validate-summary-nextflow summary-nextflow.json. The standalone summarize-nextflow bin (from @galaxy-foundry/summarize-nextflow) self-validates by default and is the better gate when the skill is also producing the summary. On schema failure, the cast skill should fail loud — the downstream Molds bind to the schema and will produce worse errors later. additionalProperties: false at every level catches drift early; do not add extra fields to work around a mismatch.
The procedure assumes — and the cast skill must surface in warnings[] when relevant — the following NF realities:
workflow { ... } block); emit a single warning and exit with the provenance block only.meta.yml may lie. nf-core module meta.yml is hand-authored and can drift from the actual script: IO. When the LLM-inferred IO disagrees with meta.yml, prefer meta.yml and surface the disagreement as a warning rather than overriding it."tuple(meta, [path,path])" is enough for downstream Molds to reason about; structured channel typing is a research project. Downstream Molds that need structure must parse the string.map { ... } with substantial Groovy logic) may produce edges flagged with low confidence in notes.include aliasing is followed one level. include { FASTP as TRIM_PROC } from '...' resolves to FASTP in processes[].name and the alias is recorded in the call graph. Multi-level aliasing chains are not chased.environment.yml files.conf/test.config + nf-test, or on snapshot/assertion patterns the structured fallback does not capture well.test_candidates[] and deciding test_selection; a profile override changes candidate configuration but does not identify a test case.