Skip to content

🐛 needgantt: address tasks by id and fix the start_date month - #1778

Merged
chrisjsewell merged 2 commits into
masterfrom
fix/needgantt-puml-generation
Aug 18, 2026
Merged

🐛 needgantt: address tasks by id and fix the start_date month#1778
chrisjsewell merged 2 commits into
masterfrom
fix/needgantt-puml-generation

Conversation

@chrisjsewell

Copy link
Copy Markdown
Member

Two defects in the PlantUML source that process_needgantt generates. Both were found
while implementing needgantt rendering in ubCode, by
rendering the byte-exact text this function emits and comparing it against what the
directive's documentation says it draws.

Both defects have the same shape: the generated source is wrong, but PlantUML does not
say so — one is silently accepted and mis-drawn, the other blows up in Python before
PlantUML ever sees it.

1. Every coloured or completed task is drawn twice — and the real bar gets neither the colour nor the completion

Tasks are declared as [<title>] as [<id>], which binds every later [...] reference to
the id. The constraint lines already used the id. The completion and colour
accumulators used the title.

PlantUML does not reject an unbound reference — it silently declares a new, zero length
task
of that name. Minimal reproduction:

@startgantt
[Title One] as [ID1] lasts 3 days
[Title One] is 80% completed
@endgantt

2 task rows. Addressing the same statement by [ID1]1 row.

Since needs_types entries carry a colour by default, this fired for essentially every
need in every chart. A three-need chart with one completion value rendered as six
bars, and the damage was not merely cosmetic: the colour and the completion attached to
the phantom bar, leaving the real one in PlantUML's default grey.

Before / after, from the generated SVG (3 needs, 2 types, one :completion: 80)
before                                    after
──────────────────────────────────────    ──────────────────────────────────────
6 task rows for 3 needs                   3 task rows for 3 needs
  Find and report bug   ← declared          Find and report bug
  Analyse bug           ← declared          Analyse bug
  Test release          ← declared          Test release
  Find and report bug   ← PHANTOM
  Analyse bug           ← PHANTOM
  Test release          ← PHANTOM

<rect fill="#E2E2F0" width="44"  …/>      <rect fill="#BFD8D2" width="35.2" …/>
   the real TASK_A bar, DEFAULT GREY         the real TASK_A bar, 80% of 44
<rect fill="#BFD8D2" width="9.6" …/>      <rect fill="#FFFFFF" width="8.8"  …/>
   the phantom, coloured, 80% of 12          the 20% remainder

Both accumulators now emit the id, like the constraint lines always did.

2. :start_date: names the wrong month, and December ends the build

utils.py defined a 0-based MONTH_NAMES starting "January"; needgantt.py indexed it
with the 1-based int(start_date.strftime("%m")).

  • :start_date: 2020-03-25Project starts the 25th of April 2020 — one month late.
  • :start_date: 2020-12-25IndexError: list index out of range, surfacing as
    ExtensionError: Handler … for event 'doctree-resolved' threw an exception. The
    build dies
    ; there is no way to put a December date on a gantt chart.

The generated statement is now the ISO date, which PlantUML also accepts:

Project starts 2020-03-25

I went for the ISO form rather than MONTH_NAMES[start_date.month - 1] because:

  • it removes the bug class instead of repairing one instance of it — there is no longer
    an index to get wrong, and MONTH_NAMES had exactly one definition and one use, no
    __all__ entry and no mention in the docs, so it is deleted;
  • it renders the identical chart. The two forms produce byte-identical SVG apart from
    the <?plantuml-src …?> processing instruction, on PlantUML 1.2026.6 and 1.2022.5
    alike, so this does not raise the effective PlantUML floor;
  • it is what the user wrote — :start_date: is already validated as %Y-%m-%d, so
    echoing it through is the smallest possible transformation, and it is locale
    independent (unlike %B);
  • it drops the bogus ordinal suffix as a side effect, without adding any suffix logic:
    the old code appended a literal th, so 2020-01-01 was written
    the 01th of February 2020.

Why the test suite never caught defect 1

tests/doc_test/utils/plantuml.jar is PlantUML 1.2022.5, and every test app is
pointed at it (tests/conftest.py:288). The phantom-task behaviour is newer than that:

source 1.2022.5 1.2026.6
current master (title-addressed) 3 rows, colours and 80% shading correct 6 rows, real bars grey
this PR (id-addressed) 3 rows, colours and 80% shading correct 3 rows, correct

1.2022.5 still resolved a [title] reference back to the aliased task; a current PlantUML
creates a second task instead. The <rect> lists for the two sources at 1.2022.5 are
byte-identical, so the fix is safe in both directions: users on an old PlantUML see no
change at all, users on a current one get the chart they always should have had.

Changed output

This changes the generated PlantUML for any chart with a type colour, a completion value
or a :start_date: — i.e. almost all of them. The rendered result is either identical
(old PlantUML) or strictly more correct (current PlantUML); nothing that rendered
correctly before renders differently now. Both changelog entries are marked
(changed output).

Tests

tests/test_needgantt.py is new — there was no needgantt test module. It follows the two
patterns already in the suite: inline files source projects (tests/test_needflow.py)
and a doctree-resolved collector for the generated PlantUML
(tests/test_filter.py::_capture_diagrams, whose docstring makes the case for asserting
on the generated source rather than the rendered image).

  • test_tasks_are_addressed_by_id asserts the general invariant — no statement may
    reference a task that is not a declared alias
    — which is precisely the property whose
    violation PlantUML answers with a phantom bar. It covers the milestone path and the
    already-correct constraint lines too.
  • test_start_date_is_the_given_date is parametrised over January, March and December,
    the last being the crash boundary.

Both fixes were mutated back out individually to confirm each test fails for its own fix
and no other (1 failed, 3 passed and 3 failed, 1 passed respectively).

Docs

The page claimed:

If svg is set as output format for PlantUML, we link the tasks elements to their
related need.

There is no calculate_link call and no [[…]] emission anywhere in needgantt.py
(unlike needflow/_plantuml.py, which does link). Task bars have never been hyperlinked;
the only link a chart produces is its caption, pointing at the generated image file. The
sentence is removed rather than implemented, as adding task links would be a feature
rather than a fix.

A third defect, left alone — float completion values

Not fixed here, to keep this PR to the two defects above, but worth recording since it is
the same class and it is currently visible in this repo's own documentation.

completion is never rounded, while duration three lines above it is, under the comment
"float durations not supported by plantuml". A number-typed completion field therefore
emits […] is 90.0% completed, which PlantUML 1.2026.6 rejects (Syntax Error? (Assumed diagram type: gantt)) though 1.2022.5 accepted it — the same version gate as
defect 1.

This is not hypothetical: docs/directives/needgantt.rst's completion_option example
uses :completion_option: amount, and docs/ubproject.toml:104 declares amount as
schema.type = "number". A docs build on a current PlantUML consequently emits
docs/directives/needgantt.rst:296: WARNING: error while running plantuml — on master
as well as on this branch (same warning, two lines further down), so this PR neither
causes nor fixes it. Happy to add the round() here or in a follow-up, whichever you
prefer.

Verification

  • New module: 4 tests, red before the fixes with the exact expected failures, green after.
  • Full suite (-m "not jstest and not benchmark"): identical FAILED set on this branch
    and on master content in the same tree — 20 pre-existing local failures either way,
    unrelated to needgantt (needuml, report_dead_links, needarch, …), and all passing when
    run in isolation. The delta is exactly the 4 new tests.
  • ruff check, ruff format, mypy clean.
  • Docs build (-nW --keep-going): 1 warning on this branch, the same 1 warning on
    master — the pre-existing float-completion one above.
  • Render checks used PlantUML 1.2026.6 and this repo's bundled 1.2022.5.

Two defects in the PlantUML source `process_needgantt` generates.

1. Tasks are declared as `[<title>] as [<id>]`, which binds every later
   `[...]` reference to the id, but the completion and colour accumulators
   addressed tasks by their title. PlantUML does not reject an unbound
   reference: it silently declares a second, zero length task of that name.
   So every need carrying a type colour or a completion value was drawn
   twice, and the colour and completion landed on the phantom bar rather
   than the real one. Both accumulators now use the id, as the constraint
   lines already did.

2. `start_date` was reformatted through `MONTH_NAMES`, a 0-based table
   indexed with the 1-based month number, so the chart started one month
   later than asked for and a December date raised `IndexError` and aborted
   the build. The generated statement is now the ISO date, which PlantUML
   also accepts and which renders the identical chart. `MONTH_NAMES` had no
   other user and is removed.

Also drops the needgantt docs' claim that task elements are linked to their
need for `svg` output; no such link has ever been generated.
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1778      +/-   ##
==========================================
+ Coverage   86.87%   90.60%   +3.72%     
==========================================
  Files          56       76      +20     
  Lines        6532    11334    +4802     
==========================================
+ Hits         5675    10269    +4594     
- Misses        857     1065     +208     
Flag Coverage Δ
pytests 90.60% <100.00%> (+3.72%) ⬆️

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 marked this pull request as ready for review August 18, 2026 20:48
@chrisjsewell
chrisjsewell merged commit b754c0a into master Aug 18, 2026
25 checks passed
@chrisjsewell
chrisjsewell deleted the fix/needgantt-puml-generation branch August 18, 2026 20:55
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.

2 participants