Skip to content

feat(graph): add dot format, file output, and step-wave overlay - #58

Merged
quike merged 2 commits into
mainfrom
feat/graph-formats
Sep 17, 2026
Merged

quike merged 2 commits into
mainfrom
feat/graph-formats

Conversation

@quike

@quike quike commented Sep 17, 2026 •

Copy link
Copy Markdown
Owner

Closes #28.

keepup graph gains --format mermaid|dot, --output <file>, a step-mode wave overlay, and annotations for conditional and cacheable groups.

It was also drawing the wrong picture

While mapping the work I found cmd/graph.go re-derived the topology with config.ExtractRefs instead of using internal/plan. The scheduler's buildDAGEdges also adds edges from when: references — so the diagram omitted dependencies the scheduler enforces.

On config-dag-when.yml, where deploy is gated on {{ eq (output "test") "pass" }}:

before:  deploy --> report                    # test --> deploy missing entirely
after:   deploy --> report
         test --> deploy                      # the when: dependency
         class deploy conditional             # and it is marked conditional

The fix is the design: the model is built from plan.Build — the same plan the engine schedules — so the picture cannot drift from what runs. That also hands over Waves for free, which is why the step overlay is small rather than new machinery.

Step mode was silent about ordering

Step mode is the default, and its waves were invisible: four sequential steps rendered as four floating nodes, reading as "these are independent".

%% before                          %% after
graph TD                           graph TD
  multi["multi"]                     subgraph wave1["step 1"]
  single["single"]                     multi["multi"]
  knobs["knobs"]                     end
  policy["policy"]                   subgraph wave2["step 2"]
  single --> knobs                     single["single"]
                                     end
                                     ...
                                     wave1 -.-> wave2
                                     wave2 -.-> wave3
                                     single --> knobs

Formats

mermaid (default) renders natively in GitHub markdown. dot covers everything else:

keepup graph ci --format dot | dot -Tsvg > docs/ci.svg

SVG is deliberately not a format we emit. Generating it means either shelling out to a dot binary we cannot assume exists or pulling in a Go layout engine — both at odds with "a single binary, no runtime". Piping to Graphviz costs users one extra command and keeps the binary unchanged.

Visual language

The two annotations are orthogonal, so a group can be both:

  • shape carries cacheable — a cylinder ([(…)] in mermaid, shape=cylinder in dot) for groups declaring cache:
  • border carries conditional — dashed, via classDef in mermaid and style=dashed in dot

In dag mode when: belongs to the group, so the node is dashed. In step mode it belongs to the step, so the wave box is dashed and its label says step 2 (conditional).

Group descriptions are still rendered, as before.

A bug the tests caught

The first dot implementation emitted clusters with no edges between them. Graphviz lays unconnected clusters out side by side — which reads as parallelism, the exact thing the overlay exists to fix. It now emits compound=true plus ltail/lhead barrier edges so the wave order is real in dot too, matching mermaid's -.->.

Structure

A new internal/graph leaf package holds the model and both renderers; cmd/graph.go drops to flag wiring and shrinks by roughly half. This keeps rendering testable without cobra and matches how cmd/ stays thin elsewhere. nodeID moved there as ident, and its test moved with it.

Golden examples

internal/graph/test-resources/ holds two realistic configs and the exact output each produces in both formats. They are tests and documentation at once — reading step.mermaid shows what a wave overlay looks like without running anything. Regenerate with go test ./internal/graph -update, and review the diff: these files are the reference for what a keepup diagram should look like, not a snapshot of whatever the renderer happens to emit.

Writing them surfaced one more bug. A group named end rendered as:

  subgraph wave1["step 1"]
    end["end"]
  end

end begins a line as both a node declaration and the subgraph terminator — ambiguous by construction. Mermaid identifiers now get a suffix when they collide with a keyword (end, graph, subgraph, class, classDef, style, click, linkStyle), while the label keeps the real name. Dot needs no equivalent since it quotes node names.

Verification

gofmt, go vet, golangci-lint, and go test -race ./... all clean.

Tests cover: the when:-derived edge, conditional and cacheable marking, wave construction including a conditional step, step data edges, both renderers in both modes, format rejection, file output and - for stdout, an unwritable path, flag completion, and determinism of model and output (a regenerated diagram must not produce a spurious diff).

Also driven through the built binary: both formats, --output to a file with stdout left empty, and --format <TAB> completing to mermaid/dot.

Rendered for real

Every golden was put through the actual renderers, not just the grammar:

File Renderer Result
step.dot, dag.dot Graphviz 16.1.0 render clean, no warnings on stderr
step.mermaid, dag.mermaid @mermaid-js/mermaid-cli render clean

The dag PNG confirms the fix visually: build (cylinder) → test → deploy (dashed) → notify, with the test → deploy edge — the one that was missing — drawn. The step PNG shows three stacked wave boxes, step 3 dashed, and build → package crossing from wave 1 to wave 3. The barrier edges carry stroke-dasharray in the SVG, so they read as ordering rather than data flow.

The end bug is confirmed real, not theoretical. Feeding Mermaid the pre-fix output:

Parse error on line 3:
Expecting 'SEMI', 'NEWLINE', ... got 'SQS'

Mermaid takes end as the keyword and chokes on the [. The post-fix output (end_["end"]) renders cleanly.

Aside

Writing the step-mode tests surfaced that a step-mode group may only reference outputs from earlier steps — my first fixture violated it and the loader caught it. Worth knowing: cross-wave data edges always point backwards in wave order.

@codecov

codecov Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.47059% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.59%. Comparing base (6cdd74f) to head (44953db).

Files with missing lines Patch % Lines
internal/graph/model.go 94.00% 3 Missing ⚠️
internal/graph/render.go 96.66% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #58      +/-   ##
==========================================
+ Coverage   92.97%   93.59%   +0.62%     
==========================================
  Files          27       29       +2     
  Lines        1565     1687     +122     
==========================================
+ Hits         1455     1579     +124     
+ Misses        107      105       -2     
  Partials        3        3              
Files with missing lines Coverage Δ
cmd/graph.go 95.00% <100.00%> (+12.24%) ⬆️
internal/graph/model.go 94.00% <94.00%> (ø)
internal/graph/render.go 96.66% <96.66%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@quike
quike merged commit 0fff562 into main Sep 17, 2026
4 checks passed
@quike
quike deleted the feat/graph-formats branch September 17, 2026 13:38
@quike

quike commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

🎉 This PR is included in version 1.32.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@quike quike added the released label Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

keepup graph: step overlay, file output, multiple formats

1 participant