Skip to content

🐛 needflow: fix legend crash, id collisions, border_color handling and highlight/warning divergences - #1768

Merged
chrisjsewell merged 22 commits into
masterfrom
claude/needflow-renderer-abstraction-v283o6
Aug 15, 2026
Merged

chrisjsewell merged 22 commits into
masterfrom
claude/needflow-renderer-abstraction-v283o6

Conversation

@chrisjsewell

@chrisjsewell chrisjsewell commented Aug 15, 2026 •

Copy link
Copy Markdown
Member

Seven independently evidenced needflow defects, each with a regression test. No new
options, no deprecations, no refactor — the two engines keep their current behaviour
everywhere else. (First slice of the needflow portable-options work; the option
abstraction itself comes separately.)

Fixes

  • :show_legend: crashed the build for a need type without a color (needflow nodes extremely dark under dark mode (Sphinx 9.1.0, Sphinx‑Needs 7.0.0) #1664).
    color is optional in needs_types, but diagrams_common.create_legend
    (plantuml) and _graphviz._create_legend both subscripted it, so the build ended with
    KeyError: 'color' on either engine. The type now keeps its legend row with a blank
    swatch instead of being dropped, and a type whose color is an empty string no longer
    emits an empty swatch value. This is the other half of needflow nodes extremely dark under dark mode (Sphinx 9.1.0, Sphinx‑Needs 7.0.0) #1664, whose fix covered the node
    colour only — its fixture had no :show_legend:, so the legend path was never covered.
  • PlantUML drew two needs as one node when their ids differed only in punctuation.
    PlantUML entity names cannot contain punctuation, so R-1 and R=1 both became R_1,
    and the second need plus its edges silently disappeared (the edge became the self-loop
    R_1 --> R_1). Entity names are now assigned per diagram from a stable, injective
    mapping. The shared make_entity_name is deliberately unchanged, because needuml
    imports it and its entity names are user-visible references there.
  • :border_color: mishandled a leading #, and a variant matching nothing.
    #00FF00 reached graphviz as ##00FF00 and PlantUML as line:#00FF00, which PlantUML
    rejects outright (the diagram failed to render). Separately, graphviz wrapped
    match_variants in str(...), so an unmatched variant expression became the literal
    colour #None. Both engines now normalise the value through one helper and add their own
    prefix, and an unset result means "no border colour" on both. The two graphviz code paths
    — plain node and subgraph — are covered by the same test and must agree.
  • :highlight: filters that consult needs failed for a need with parts or children.
    Graphviz draws such a need as a subgraph, and that path called filter_single_need
    without the needs list, while the plain-node path passed it. An expression like
    len(needs) > 1 therefore raised NeedsInvalidFilter: name 'needs' is not defined and
    failed the whole build, purely because a need happened to have a child.
  • A graphviz needflow image had no alternative text. The directive always stored an
    alt, so the intended alt="needflow graphviz diagram" default was unreachable and
    every image was published with alt="". The placeholder now applies when :alt: is
    absent; writing :alt: with no value still gives alt="" for a diagram that is purely
    decorative. This is the one user-visible rendering change in the PR.
  • Two warning defects. The unknown-:config: warning named need_flows_configs,
    which does not exist (it is needs_flow_configs), and the graphviz unknown-:link_types:
    warning was emitted without a location, so it arrived with no file:line — unlike the
    plantuml one.

Environment version

ENV_DATA_VERSION goes 5 → 6, because the alt fix changes how the option is recorded
when it is not given ("" → None) and that value is persisted in doctrees. Without the
bump the two halves can disagree within one version: an older reader over a newer doctree
publishes the literal alt="None", and a newer reader over an older doctree leaves the fix
silently inert. The first build after upgrading re-reads every document.

Tests

  • tests/test_github_issues.py::test_doc_github_1664_legend (both engines), on the
    existing doc_test/doc_github_issue_1664 project extended with a :show_legend: page.
  • Seven new tests in tests/test_needflow.py, parameterised over both engines where the
    behaviour is shared, asserting on the :debug: diagram source (and, for alt, on the
    built <img>): node-id injectivity, border_color normalisation, highlight with
    needs, both :config: warning texts, the link-type warning location, the three
    alt cases, and the entity-name fallback contract (value + warning) covered directly.
  • Each behavioural fix's test was recorded failing against the unfixed code before its
    fix landed; the commits are ordered test-then-fix per defect.
  • The plantuml entity-name lookup warns if an id reaches it unmapped, which cannot happen
    today — the two assert warnings == "" tests in tests/test_needflow.py are what keeps
    it that way, and they stay green.
  • The full suite passes (1214 passed, 11 skipped) with no snapshot changes.

Documentation

docs/changelog.rst gains a Bug fixes section under Unreleased, and the alt section
of docs/directives/needflow.rst now documents the graphviz default and the
explicitly-empty case. The border_color reference needs no change: accepting a leading
# is a tolerance, not a contradiction of what it documents.

@codecov

codecov Bot commented Aug 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.14%. Comparing base (4e10030) to head (5d19b44).
⚠️ Report is 320 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1768      +/-   ##
==========================================
+ Coverage   86.87%   90.14%   +3.26%     
==========================================
  Files          56       74      +18     
  Lines        6532    11128    +4596     
==========================================
+ Hits         5675    10031    +4356     
- Misses        857     1097     +240     
Flag Coverage Δ
pytests 90.14% <100.00%> (+3.26%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chrisjsewell
chrisjsewell merged commit a1c0b30 into master Aug 15, 2026
25 checks passed
@chrisjsewell
chrisjsewell deleted the claude/needflow-renderer-abstraction-v283o6 branch August 15, 2026 11:39
chrisjsewell added a commit that referenced this pull request Aug 15, 2026
#### What moved where

Both needflow engines independently resolved the allowed link types,
walked out from
`root_id`, filtered, built the nesting tree, resolved every node's
presentation and
collected the edges — in code that had drifted apart (#1768 fixed the
divergences that
had already become bugs).

All of that now happens once, in a new
`sphinx_needs/directives/needflow/_model.py`, which
hands the engines a graph of dumb dataclasses (`NeedflowGraph`,
`GraphNode`, `GraphEdge`,
`NodePresentation`) holding resolved values and no syntax: a colour is a
colour, not
`#FF0000` or `line:FF0000`, and nesting is a tree, not braces or a
subgraph.

The emitters keep only what is genuinely theirs:

- `_plantuml.py` (553 → 420 lines): source text, the injective entity
naming, brace nesting,
  the `' Config` block, legend/debug/caption/align/scale, DOM wiring.
- `_graphviz.py` (739 → 632 lines): source text, `subgraph cluster_*` +
the invisible ghost
node + `ltail`/`lhead`, the plantuml→dot shape and arrow translation
tables,
  `graphviz_style`, its own legend/debug, the HTML visitor.
- `_shared.py` (115 → 44 lines): just the "Used filter:" paragraph, the
one piece of
  docutils content neither engine writes in diagram syntax.

`resolve_link_types` is deliberately a separate entry point from
`build_graph`, so that
plantuml keeps warning about link types BEFORE its
`sphinxcontrib.plantuml` import guard.

#### The zero-delta proof

Behaviour is frozen, and the gate is the generated diagram source
itself. A harness
captures every `@startuml…@enduml` and every `digraph needflow {…}` at
the processor level,
for EVERY `tests/doc_test` project containing a needflow, the whole
`docs/` build, and two
purpose-built synthetic projects covering the option paths the fixtures
miss — each built
with BOTH engines: 21 projects × 2 engines = 42 builds, 206 diagram
sources.

Baseline captured at master, re-run after every commit: zero byte
deltas, no exceptions.
The full Sphinx warning stream is compared as a second channel and is
also byte-identical,
which is what fences "no new warnings on any default path". The gate
itself was
review-verified: deliberate mutations (a one-character emission change;
a deleted emitter
call) turn it red in both the content and the missing-diagram direction,
and its capture
seam was independently checked against the real renderer boundaries at
both the base and
the tip.

#### The one normalisation

The directive defaulted `root_direction` to `"all"`, a value outside the
`Literal["both", "incoming", "outgoing"]` its own `NeedsFlowType`
declares;
`filter_by_tree` has always treated it identically to `"both"` via the
else-branch. The
default is now `"both"` — the runtime now matches the declared type, and
the byte gate
proves the graphs are unchanged. No TypedDict shape change, so no
`ENV_DATA_VERSION`
implication.

#### What stayed frozen, and why

This slice exists so the NEXT one (a portable option vocabulary) can be
implemented once
instead of twice. Fixing behaviour here would have destroyed the only
proof available that
the extraction is faithful. So these are reproduced exactly, and now
documented where they
happen rather than being folklore:

- `filter_by_tree` walks depth-first (`dict.popitem`) with
first-visit-wins, so
  `root_depth` is traversal-order dependent.
- `parent_needs` is stripped from the allowed link types BEFORE the root
walk, so
  `root_id` cannot follow the need hierarchy.
- The root walk runs before the filter.
- `show_link_names` is OR-ed with `needs_flow_show_links`, so config can
only turn labels on.
- A plantuml edge is emitted even when an end is not drawn (plantuml
then invents a bare
node); graphviz drops it. The model records
`source_drawn`/`target_drawn` and lets each
  engine keep its rule.
- The graphviz engine used to test for an empty result before applying
the `max_items`
cap, and the model now applies the cap first. The verdict is unchanged
either way:
`apply_max_items` returns the needs untouched or truncates them to a
limit of at least
  one, so it can never turn a non-empty result into an empty one.
- A need whose parts and children were all filtered out still gets its
(empty) plantuml
nesting block and its `' parts:` comment: those are decided from the
need, not from what
  is drawn.
- A need drawn as a graphviz subgraph emits its `type_style`
untranslated and unwarned,
  unlike a plain node.
- The per-engine legend content, debug node type, caption/align/scale
handling and
  `:config:` namespaces are untouched, in their own emitters.

#### Tests

`tests/test_needflow_model.py` — 47 new unit tests, 0.2s, no
documentation project built:
the walk (direction, depth, cycles, links out of the view),
allowed-link-type resolution,
presentation (highlight beating border colour, part→rectangle, colour
normalisation), the
nesting tree, and the edge membership rule. Purely additive; no existing
assertion was
touched. The full suite passes (1337 passed, 11 skipped).

**Changelog**: one bullet under `Unreleased` → `Internal changes`, the
section this
repository keeps for changes that do not affect user-facing behaviour:
♻️ needflow's two
engines now share one graph-model pass, with no change to the generated
diagram source.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01BVNJ8SV3EVcoc6TdLVmnVn)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants