Skip to content

Fix tree rendering of shared dependencies (diamond graphs)#237

Merged
ahogappa merged 1 commit into
masterfrom
fix/tree-shared-dep-prefix
Jun 13, 2026
Merged

Fix tree rendering of shared dependencies (diamond graphs)#237
ahogappa merged 1 commit into
masterfrom
fix/tree-shared-dep-prefix

Conversation

@ahogappa

@ahogappa ahogappa commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Problem

Found while recording the theme demo for #235/#236pre-existing on master (verified byte-identical there). When a task is a shared dependency (diamond: Root → Left → Shared and Root → Shared), the rendered tree duplicates the row with a broken prefix:

Root                           Root
├── Left                       ├── Left
└── Shared        instead of   │   └── Shared
└── Shared                     └── Shared

register_tree_nodes keyed depth/is_last/ancestors_last by task class, so the last-registered occurrence clobbered every earlier one — all rows for that class rendered with the same prefix.

Fix

The prefix belongs to the occurrence, not the class: it is computed at registration and stored on the tree node; build_node_lines reads it from the node. The class-keyed map keeps only the first occurrence's prefix as the one canonical prefix for Tree::Event's per-event lines (a shared task runs once; its event line prints at the position it first appears in the tree).

Second bug fixed by the same first-wins change (found by the adversarial review): when the root class reappears as a circular leaf (Root → A → RootTask.tree renders cyclic graphs by design, cycle validation only runs at execution), master clobbered the root's node with the childless circular leaf, so build_tree_lines walked the leaf and the whole tree collapsed to a single orphan line ( └── Root). Now renders the full tree with the cycle as a leaf occurrence.

Affects render_tree (Task.tree, Tree::Live live/final frames) and Tree::Event event lines.

Tests

Adversarial review

28-agent review across the three follow-up PRs: for this PR, deep diamonds / multiple shared nodes / shared subtrees / circular markers / Live lifecycle all verified correct, non-shared output byte-identical to master. 1 confirmed finding (the untested root-cycle behavior above) — folded in.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ahogappa, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 51 minutes and 40 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a3eb197d-f378-47cc-9e4a-0052ffcccbb7

📥 Commits

Reviewing files that changed from the base of the PR and between 3968cf7 and b714bd0.

📒 Files selected for processing (2)
  • lib/taski/progress/layout/tree/structure.rb
  • test/test_layout_tree_event.rb

Walkthrough

This PR refactors how tree-line prefixes are managed: instead of computing them from task-class state on-the-fly, prefixes are now computed once during node registration and stored directly on each node. The prefix API is split into a lookup function and a new helper for computing prefixes from traversal state. Tests validate correct rendering of shared task occurrences with their position-specific prefixes.

Changes

Tree Prefix Storage and Computation Refactoring

Layer / File(s) Summary
Node prefix storage and registration
lib/taski/progress/layout/tree/structure.rb
Tree initialization creates a @node_prefixes map; node registration computes a prefix at registration time, stores it on the node as node[:prefix], and records the first-occurrence canonical prefix per task class; rendering reads the stored prefix directly instead of recomputing from task-class state.
Prefix computation API refactoring
lib/taski/progress/layout/tree/structure.rb
build_tree_prefix(task_class) now returns the canonical prefix from @node_prefixes, and a new compute_tree_prefix(depth:, is_last:, ancestors_last:) helper generates the visual prefix from traversal state parameters.
Shared dependency rendering tests
test/test_layout_tree_event.rb
Two new tests validate that shared tasks render with their own position-specific tree prefix at each occurrence and that event updates to shared tasks use the prefix from the task's first occurrence in the tree.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • ahogappa/taski#181: Both PRs modify lib/taski/progress/layout/tree/structure.rb's prefix-rendering logic; this PR refactors how per-node prefixes are stored and computed while that PR establishes the prefixing pipeline.

Poem

🐰 Prefixes now find their home
Stored where each node can roam
No recompute each call—
One storage holds them all
And shared tasks bloom with their own visual sprawl!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: addressing tree rendering issues with shared dependencies in diamond-shaped dependency graphs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/tree-shared-dep-prefix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

A shared dependency (Root -> Left -> Shared, Root -> Shared) is
registered once per occurrence when the dependency DAG is expanded into
a tree, but register_tree_nodes keyed depth/is_last/ancestors_last by
TASK CLASS — so the last occurrence clobbered every earlier one and all
rows for that class rendered with the same (wrong) prefix:

    Root                          Root
    ├── Left                      ├── Left
    └── Shared        instead of  │   └── Shared
    └── Shared                    └── Shared

The prefix belongs to the occurrence: it is now computed at registration
and stored on the tree node itself; build_node_lines reads it from the
node. The class-keyed map keeps only the FIRST occurrence, as the one
canonical prefix for Tree::Event's per-event output lines (a shared
task runs once, so its event line prints at the position it is first
shown in the tree).

The same first-wins change to @tree_nodes fixes a second, more severe
class-keying bug: when the ROOT class reappears as a circular leaf
(Root -> A -> Root), master clobbered the root's node with the
childless leaf, so build_tree_lines walked the leaf and Task.tree
collapsed to a single orphan line ("    └── Root"). Both behaviors are
pinned: an exact full-tree diamond assertion, an event-line assertion,
and an exact full-tree root-cycle assertion.

Affects render_tree (Task.tree, Tree::Live live/final frames) and
Tree::Event event lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ahogappa
ahogappa force-pushed the fix/tree-shared-dep-prefix branch from 3a809d8 to b714bd0 Compare June 13, 2026 22:36
@ahogappa
ahogappa merged commit def9020 into master Jun 13, 2026
8 checks passed
@ahogappa
ahogappa deleted the fix/tree-shared-dep-prefix branch June 13, 2026 22:46
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.

1 participant