LAYOUT_REDO

Layout Redo: property-tested layered layout for gxwf-layout

Repo: gxformat2 · Branch: layout (already has the v1 gxwf-layout feature committed as dd231d4, pushed to jmchilton/layout) Date: 2026-06-30 Scope: drop the byte-identical cross-language coordinate contract for the bake path, replace it with a property-based declarative test contract, and add a second, nicer in-house layout strategy (layered, barycenter Sugiyama) alongside the existing topological.


Why

gxwf-layout (new in this branch) bakes {left, top} position records into Format2/native workflow docs, retiring the degenerate (10*i, 10*i) diagonal fallback. v1 ships exactly one strategy, topological, because it was the only layout that:

  1. computes coordinates in pure Python (cytoscape’s dagre/cose/etc. are JS, render-time only — see cytoscape/_layout.py:bakes_coordinates), and
  2. is byte-identical with the TypeScript port (galaxy-tool-util-ts cytoscape-layout spec).

topological is a strict Kahn topo-sort: column = longest-path depth, row = declaration order within column. It has no crossing minimization — legible but plain, more edge crossings than dagre on wide/real (IWC) workflows.

We are uninterested in byte-identical for the bake path. Dropping it unlocks a better layout in Python without a lockstep JS port. The cross-language guarantee becomes “both implementations satisfy the same structural properties,” tested via the existing declarative YAML framework, extended with graph-level predicates.


Decisions locked


Architecture

Two contracts, kept cleanly separate:

LayerFileContract
topological coordinate mathgxformat2/cytoscape/_layout.pybyte-identical with TS port; untouched this pass — cycle policy enforced upstream in the layout module, not here
layered (barycenter Sugiyama)gxformat2/layout/_sugiyama.py (new)not byte-identical; validated by structural properties only
property checkersgxformat2/testing.py (extended)language-agnostic spec; each language reimplements checkers

gxformat2/layout/_builder.py:layout_positions() dispatches on strategy:

Both reuse cytoscape_elements(nf2, layout="preset") for node/edge extraction, so there is one node-id + edge derivation, not a third (continues the single-source theme from the v1 review).


Work breakdown (red → green per phase)

Phase 1 — Cycles fail (cycle policy lives in gxformat2/layout, cytoscape untouched)

Phase 2 — graph_property assertion category in testing.py

Phase 3 — layered strategy (in-house barycenter Sugiyama)

Phase 4 — Rewrite expectations/layout.yml to properties

Phase 5 — Docs + lint/type/test sweep


Files touched

FileChange
gxformat2/cytoscape/_layout.pyuntouched (Q1) — keeps its viz fallback + TS byte-identical contract
gxformat2/layout/_sugiyama.pynew — shared layering (raises LayoutCycleError) + barycenter layered_positions; grandalf-unmaintained comment
gxformat2/layout/_builder.pyup-front cycle detection for both strategies; dispatch layered; layout_positions strategy branch
gxformat2/layout/_cli.pyadd layered to --strategy choices
gxformat2/layout/__init__.pyexport layered_positions, LayoutCycleError
gxformat2/testing.pygraph_properties assertion category + 4 registered checkers
gxformat2/examples/expectations/layout.ymlproperties instead of exact coords; _layered cases; cyclic expect_error
tests/test_layout.pyPython-local exact-coord goldens + determinism + cycle raises
tests/test_interop_tests.pyregister layout_layered_format2 / layout_layered_native operations
gxformat2/examples/format2/synthetic-cycle.gxwf.ymlnew cyclic fixture
gxformat2/examples/format2/synthetic-crossing-*.gxwf.ymlnew crossing-heavy fixture
gxformat2/examples/catalog.ymlregister new fixtures
docs/cli_layout.rsttopological vs layered prose

Testing strategy


Resolved decisions (were open questions)

  1. Cycle raise location → layout module, cytoscape untouched. layout_positions detects cycles up front and raises LayoutCycleError for both strategies; cytoscape/_layout.py keeps its viz fallback and TS byte-identical contract. No lockstep coordination this pass.
  2. deterministic property → Python-local imperative test in test_layout.py, not a graph_property (the runner yields a single result; determinism needs two runs).
  3. layered coordinate pass → simple order-index rows first. Properties hold without nudging; priority/barycenter nudging is an aesthetic follow-up that won’t change the contract.
  4. Strategy in declarative tests → separate named operations (layout_layered_format2 / layout_layered_native), no runner change. The runner calls operation(fixture) with one arg and TestCase has no params field; named ops match existing style.
  5. CLI default → keep topological default (dep-free, simplest); layered is opt-in. Revisit flipping once layered is proven on the IWC corpus.
  6. gxwf-viz exposure → gxwf-layout-only this pass. Don’t add layered to cytoscape viz now (consistent with leaving cytoscape/_layout.py untouched).

Remaining open questions