Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

### Fixed

- **Diagram IR rendering robustness**: `diagram --from-ir` now accepts common `source` / `target` edge aliases in addition to the canonical `from` / `to` keys, and reports malformed edges as clean CLI errors instead of tracebacks.
- **Fresh-layout docs and agent entries**: Slimmed agent entry docs into lightweight navigation surfaces, moved deeper runtime guidance into `docs/guide/agent-reference.md`, and aligned README, CLI docs, skills, and setup docs around explicit migration instead of implicit legacy runtime reads.
- **Skill metadata and routing docs**: Normalized active project skill frontmatter to the cross-agent `name` + `description` shape, made descriptions trigger-focused, refreshed the skill harness validator, and fixed router wording that could steal explicit review-response requests.
- **Runtime-layout migration correctness**: Fixed migration verification against real migrated libraries, empty legacy root cleanup, workspace output migration, and recovery/finalization edge cases found during repeated live CLI rehearsals.
Expand Down
23 changes: 22 additions & 1 deletion scholaraio/services/diagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,27 @@ def wrapper(fn: Renderer) -> Renderer:
return wrapper


def _normalize_ir_edges(ir: dict) -> dict:
"""Return an IR copy whose edges use the canonical from/to keys."""
normalized = dict(ir)
edges = []
for idx, edge in enumerate(ir.get("edges", []), start=1):
if not isinstance(edge, dict):
raise ValueError(f"Invalid diagram IR edge #{idx}: expected an object")
current = dict(edge)
if "from" not in current and "source" in current:
current["from"] = current["source"]
if "to" not in current and "target" in current:
current["to"] = current["target"]
if "from" not in current or "to" not in current:
raise ValueError(
f"Invalid diagram IR edge #{idx}: each edge must include from/to (source/target aliases are accepted)"
)
edges.append(current)
normalized["edges"] = edges
return normalized


def list_renderers() -> list[str]:
"""返回当前支持的所有渲染格式列表。"""
return list(_RENDERERS.keys())
Expand Down Expand Up @@ -570,7 +591,7 @@ def render_ir(ir: dict, fmt: str, out_path: Path | None = None) -> Path | str:
"""
if fmt not in _RENDERERS:
raise ValueError(f"Unsupported render format: {fmt} (supported: {', '.join(_RENDERERS.keys())})")
return _RENDERERS[fmt](ir, out_path)
return _RENDERERS[fmt](_normalize_ir_edges(ir), out_path)


# ---------------------------------------------------------------------------
Expand Down
23 changes: 23 additions & 0 deletions tests/test_diagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,29 @@ def test_unsupported_format(self, sample_ir):
with pytest.raises(ValueError, match="Unsupported render format"):
render_ir(sample_ir, "png")

def test_source_target_edge_aliases_are_accepted(self):
ir = {
"title": "Alias",
"nodes": [{"id": "a", "label": "A"}, {"id": "b", "label": "B"}],
"edges": [{"source": "a", "target": "b", "label": "flow"}],
"layout_hint": "horizontal",
}

text = render_ir(ir, "mermaid")

assert 'a -->|"flow"| b' in text

def test_invalid_edge_reports_clean_error(self):
ir = {
"title": "Bad Edge",
"nodes": [{"id": "a", "label": "A"}],
"edges": [{"source": "a"}],
"layout_hint": "horizontal",
}

with pytest.raises(ValueError, match="each edge must include from/to"):
render_ir(ir, "mermaid")


# ---------------------------------------------------------------------------
# generate_diagram
Expand Down
Loading