Skip to content

fix(stage5): dedupe merged arrows by (src, op, dst) tuple#65

Merged
RichardHightower merged 1 commit into
mainfrom
fix/stage5-dedupe-arrows-by-tuple
May 14, 2026
Merged

fix(stage5): dedupe merged arrows by (src, op, dst) tuple#65
RichardHightower merged 1 commit into
mainfrom
fix/stage5-dedupe-arrows-by-tuple

Conversation

@RichardHightower
Copy link
Copy Markdown
Contributor

Summary

Replace Stage 5's full-line-string arrow dedupe with a (src, op, dst) tuple key so labelled variants like A --> B : foo and A --> B : bar collapse to one canonical edge.

Why

Discovered during the agent-brain trust-eval pass (docs/gen/agent-brain/EVAL.md). The _merge_class_diagrams helper used a set[str] keyed on the full line, so two arrows with the same (source, op, target) but different labels survived as distinct entries. Live in docs/gen/agent-brain/packages/models/README.md:

BaseModel <|-- GraphQueryContext : extends
BaseModel <|-- GraphQueryContext : inherits
Loading

Five duplicate-edge pairs across the models overview alone (GraphQueryContext, GraphTriple, CodeChunkStrategy, QueryMode, JobRecord *-- JobProgress). The diagram still renders but the noise undermines the overview's job of being a clean bird's-eye view. This is the same class of issue I papered over in #60 with the post-strip case in config; the eval caught that it also fires without label stripping in models.

Closes #62.

Changes

  • src/designdoc/stages/s5_mermaid.py
    • New _parse_arrow(line) — returns (src, op, dst, label) or None. Sorts _ARROW_OPS longest-first as defence against future ops that might become substrings of one another (no current op is, but it's free insurance).
    • _merge_class_diagrams replaces arrows: set[str] with arrow_keys: set[tuple[str, str, str]] + arrow_lines: list[str]. First label seen wins — deterministic given the caller's stable input order (sorted class_docs).
  • tests/unit/test_stage5_package_diagrams.py
    • 7 new tests: parser correctness across all 10 relationship ops, label-variant collapse, distinct-op survival, distinct-target survival, malformed-line None return.
    • All existing tests (identical-arrow dedupe, arrow preservation, non-classDiagram skip, etc.) continue to pass without modification.

Invariants

  • MAX_ATTEMPTS=3 preserved (loop.py untouched)
  • Checker isolation preserved (no doer/checker context bridging)
  • Schema-validated verdicts / fail-loud preserved
  • Mermaid two-checker (mmdc + LLM) preserved — change is in post-processing only; both checkers still run against the per-class diagrams unchanged
  • HIL fallback preserved

Verification

  • task ci green locally (107 passed in 56.76s)
  • 19 unit tests in test_stage5_package_diagrams.py pass (12 prior + 7 new)
  • TWRC discipline followed (RED with ImportError → implementation → GREEN)
  • Manual rerun against agent-brain target — will verify that packages/models/README.md loses the 5 duplicate-edge pairs (not part of merge gate; the unit tests prove the behaviour)

Related

Stage 5's per-package merger deduplicated relationship arrows by full line
string (`set[str]`). Two arrows with the same (source, op, target) but
different labels survived as distinct entries, producing visible
duplicates in the package overview.

Live example from agent-brain `packages/models/README.md`:

    BaseModel <|-- GraphQueryContext : extends
    BaseModel <|-- GraphQueryContext : inherits

Five duplicate-edge pairs visible across that one package (GraphQueryContext,
GraphTriple, CodeChunkStrategy, QueryMode, JobRecord *-- JobProgress).

Replace the set with a (src, op, dst) tuple key. First label seen wins —
deterministic given the stable input order (sorted class_docs in the
caller). Genuinely distinct edges (different op, different target)
still survive.

The new _parse_arrow helper sorts _ARROW_OPS longest-first as a defensive
measure even though no current op is a substring of another — this
protects future _ARROW_OPS additions from substring-matching surprises.

Existing tests for identical-arrow dedupe and arrow-preservation continue
to pass. New tests:

  * _parse_arrow on all 10 relationship ops
  * dedupe collapses labelled variants to one arrow (first wins)
  * distinct-op and distinct-target edges are preserved

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@RichardHightower RichardHightower force-pushed the fix/stage5-dedupe-arrows-by-tuple branch from 500e8c3 to a256b2d Compare May 14, 2026 21:39
@RichardHightower RichardHightower merged commit 832a1f2 into main May 14, 2026
2 checks passed
@RichardHightower RichardHightower deleted the fix/stage5-dedupe-arrows-by-tuple branch May 14, 2026 21:41
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.

Stage 5: dedupe merged arrows by (src, op, dst) — labelled variants survive

1 participant