galaxyproject / galaxyproject/gxformat2

Diagram builders drop step→step edges into steps that carry a distinct label

Open
#228 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
14
Forks
7
Avg merge
2h 49m
Merged PRs (30d)
8

Description

> **Posted by Claude (AI assistant) on behalf of @jmchilton — not authored by them personally.**

## Summary

The Mermaid and Cytoscape diagram builders silently drop (or dangle) step→step edges whose source addresses an upstream step by its **`id`** when that step carries a distinct **`label:`**. The downstream node ends up orphaned (Mermaid) or the edge points at a non-existent node id (Cytoscape).

Both builders key step nodes by `step.label or step.id`, but resolve `in:` source references against `NormalizedFormat2.known_labels`, which is `{s.label or s.id} | {input ids}` — it does **not** include a step's `id` when a distinct `label` is present. So a source like `upstream/out` (referencing the step's `id`) matches nothing, falls through `resolve_source_reference`, and the resulting `step_label` never matches a keyed node.

Input→step edges are unaffected (inputs are both referenced and keyed by id), so diagrams just look deceptively sparse rather than erroring.

## Reproduction

```python
from gxformat2.mermaid import workflow_to_mermaid
from gxformat2.cytoscape import cytoscape_elements

wf = {
"class": "GalaxyWorkflow",
"inputs": {"in_file": {"type": "data"}},
"outputs": {},
"steps": {
"upstream": {
"label": "Upstream Tool With Label",
"tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/up/up/1.0",
"tool_version": "1.0",
"in": {"input": "in_file"},
"out": [{"id": "out"}],
},
"downstream": {
"label": "Downstream Tool With Label",
"tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/down/down/1.0",
"tool_version": "1.0",
"in": {"data": "upstream/out"}, # references upstream by its dict id
"out": [{"id": "result"}],
},
},
}

print(workflow_to_mermaid(wf))
```

### Mermaid — actual

```
graph LR
input_0>"in_file
data"]
step_0["Upstream Tool With Label"]
step_1["Downstream Tool With Label"]
input_0 --> step_0
```

`step_0 --> step_1` is missing — `downstream` renders orphaned.

### Cytoscape — actual

```
node ids: {'in_file', 'Downstream Tool With Label', 'Upstream Tool With Label'}
edge source='in_file' target='Upstream Tool With Label' dangling=False
edge source='upstream' target='Downstream Tool With Label' dangling=True
```

The second edge's `source='upstream'` is the raw dict id; no node carries that id (nodes are keyed by label), so it dangles.

### Expected

A `step_0 --> step_1` edge in Mermaid, and a Cytoscape edge whose `source` is the upstream node's render identity (`"Upstream Tool With Label"`) — both endpoints real nodes.

Verified against `gxformat2` @ `53f1624` (Python 3.12).

## Root cause (code pointers)

- `gxformat2/normalized/_format2.py` — `known_labels` = `{s.label or s.id} | {input ids}`; a step's `id` is omitted whenever it has a distinct `label`.
- `gxformat2/mermaid/_builder.py` — `step_ids[step.label or step.id] = node_id`; edges look up `step_ids.get(source_ref.step_label)`, which is `None` for an `id`-form source.
- `gxformat2/cytoscape/_builder.py` — same keying (`step_id = step.label or step.id`); `_step_edges` emits `source=ref.step_label` verbatim, producing the dangling edge.

## Suggested fix

Index steps by their `id` **as well as** their render identity, and fold an `id`-form source reference back to the render identity (`label or id`) the node is keyed by before lookup. The common case (`label == id`) is unchanged.

For reference, this was found and fixed in the TypeScript port (galaxy-tool-util-ts) by adding an `id → render-identity` map alongside `known_labels` and folding the resolved source through it in both builders. Happy to open a PR here mirroring that approach if useful.

## Note / open question

Whether such a workflow (explicit `label:` on a dict-form step, referenced elsewhere by the dict key) is *intended* to be valid format2 is worth confirming — but since these are visualization helpers, rendering the connection seems strictly better than silently orphaning a node or emitting a dangling edge.

Contributor guide

Open the contributing guide

Research direction

Start with gxformat2/normalized/_format2.py, then trace the step keying and edge resolution in gxformat2/mermaid/_builder.py and gxformat2/cytoscape/_builder.py. Run the Python reproduction from the issue and verify that the upstream-to-downstream edge connects the real rendered nodes in both builders without dangling endpoints.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
data-visualization
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.