2026-03-16 12:55:34 +00:00
|
|
|
|
---
|
|
|
|
|
|
id: MRKD-WP-0005
|
|
|
|
|
|
type: workplan
|
2026-06-22 23:16:27 +02:00
|
|
|
|
domain: communication
|
2026-03-16 12:55:34 +00:00
|
|
|
|
repo: marki-docx
|
2026-03-17 12:16:09 +00:00
|
|
|
|
status: done
|
2026-03-16 12:55:34 +00:00
|
|
|
|
state_hub_workstream_id: 2ef47f11-d828-436d-8955-c58e13c50752
|
|
|
|
|
|
created: 2026-03-16
|
2026-03-17 12:16:09 +00:00
|
|
|
|
updated: 2026-03-17
|
2026-03-16 12:55:34 +00:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# MRKD-WP-0005 — Diagram Renderer Integration
|
|
|
|
|
|
|
|
|
|
|
|
Complete the deferred rendering path for FR-533/534. Currently `diagrams.py` operates
|
|
|
|
|
|
in source-only mode: diagram fenced blocks are preserved as source through the round-trip
|
|
|
|
|
|
but are never rendered to images. This workstream adds actual rendering support for
|
|
|
|
|
|
Mermaid, Graphviz, and PlantUML when the corresponding CLI tools are available, while
|
|
|
|
|
|
preserving the graceful source-only fallback when they are not.
|
|
|
|
|
|
|
|
|
|
|
|
**Scope:** FR-533 (auto-diagram support), FR-534 (diagram intent preservation),
|
|
|
|
|
|
FR-538 (processor-dependency disclosure)
|
|
|
|
|
|
**Out of scope:** new diagram syntaxes beyond mermaid/graphviz/plantuml
|
|
|
|
|
|
**Depends on:** MRKD-WP-0003 (diagrams.py source-only foundation) — complete
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## T01 — Renderer abstraction layer and detection
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: MRKD-WP-0005-T01
|
2026-03-17 12:16:09 +00:00
|
|
|
|
status: done
|
2026-03-16 12:55:34 +00:00
|
|
|
|
priority: high
|
|
|
|
|
|
state_hub_task_id: c4911ecc-1e3c-4d22-a6fb-92d1ed319274
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Introduce a `RendererBackend` abstraction in `diagrams.py` so that each diagram type
|
|
|
|
|
|
can be rendered by a pluggable backend. The builder calls the abstraction; concrete
|
|
|
|
|
|
backends handle tool detection and subprocess invocation.
|
|
|
|
|
|
|
|
|
|
|
|
- `DiagramRenderer` protocol: `can_render(diagram_type: str) -> bool`,
|
|
|
|
|
|
`render(source: str, diagram_type: str, output_path: Path) -> RendererResult`
|
|
|
|
|
|
- `RendererResult`: `success: bool`, `output_path: Path | None`, `warning: WarningRecord | None`
|
|
|
|
|
|
- `detect_renderers() -> dict[str, DiagramRenderer]` — probe PATH for `mmdc`, `dot`,
|
|
|
|
|
|
`plantuml`; return only those found
|
|
|
|
|
|
- Builder updated to call `detect_renderers()` at build time; if a renderer is found
|
|
|
|
|
|
for the diagram type → render to PNG and embed; if not → source-only fallback +
|
|
|
|
|
|
`WarningRecord(reason="renderer-unavailable", construct=diagram_type)` (FR-538)
|
|
|
|
|
|
- Unit tests: `test_renderer_detection` — mock PATH; `test_fallback_when_no_renderer`
|
|
|
|
|
|
|
|
|
|
|
|
Deliverable: `pytest tests/test_level3_diagrams.py` still passes; renderer abstraction
|
|
|
|
|
|
in place and tested.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## T02 — Mermaid CLI (mmdc) integration
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: MRKD-WP-0005-T02
|
2026-03-17 12:16:09 +00:00
|
|
|
|
status: done
|
2026-03-16 12:55:34 +00:00
|
|
|
|
priority: high
|
|
|
|
|
|
state_hub_task_id: 87caa295-f466-4e2e-ba06-4b1a801ca976
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Implement the `MermaidRenderer` backend that shells out to `mmdc` (Mermaid CLI).
|
|
|
|
|
|
|
|
|
|
|
|
- `MermaidRenderer.can_render("mermaid") -> bool`: checks `shutil.which("mmdc")`
|
|
|
|
|
|
- `MermaidRenderer.render(source, "mermaid", output_path)`:
|
|
|
|
|
|
- Write source to a temp `.mmd` file
|
|
|
|
|
|
- Run `mmdc -i <input> -o <output>.png`
|
|
|
|
|
|
- On success: return `RendererResult(success=True, output_path=...)`
|
|
|
|
|
|
- On failure: return `RendererResult(success=False, warning=WarningRecord(reason="render-failed"))`
|
|
|
|
|
|
- Builder: PNG embedded into DOCX as image; alt-text carries `[mermaid-source]<source>` marker
|
|
|
|
|
|
for round-trip (FR-534)
|
|
|
|
|
|
- Importer: existing alt-text marker detection already handles this — verify round-trip works
|
|
|
|
|
|
- Add `mmdc` to `pyproject.toml` optional extras: `diagram-mermaid`
|
|
|
|
|
|
- Integration test (skipped if `mmdc` not found): `test_mermaid_render_roundtrip`
|
|
|
|
|
|
|
|
|
|
|
|
Deliverable: When `mmdc` is on PATH, mermaid diagrams render to PNG and embed in DOCX;
|
|
|
|
|
|
round-trip restores source. When absent, source-only fallback with warning.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## T03 — Graphviz (dot) integration
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: MRKD-WP-0005-T03
|
2026-03-17 12:16:09 +00:00
|
|
|
|
status: done
|
2026-03-16 12:55:34 +00:00
|
|
|
|
priority: medium
|
|
|
|
|
|
state_hub_task_id: 8ceb771d-0b16-452d-b1cc-c5c2c40fe723
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Implement the `GraphvizRenderer` backend that shells out to `dot`.
|
|
|
|
|
|
|
|
|
|
|
|
- `GraphvizRenderer.can_render("graphviz") -> bool`: checks `shutil.which("dot")`
|
|
|
|
|
|
- `GraphvizRenderer.render(source, "graphviz", output_path)`:
|
|
|
|
|
|
- Write source to a temp `.dot` file
|
|
|
|
|
|
- Run `dot -Tpng <input> -o <output>.png`
|
|
|
|
|
|
- On success / failure: same pattern as MermaidRenderer
|
|
|
|
|
|
- Alt-text marker: `[graphviz-source]<source>` for round-trip
|
|
|
|
|
|
- Add `graphviz` to optional extras: `diagram-graphviz`
|
|
|
|
|
|
- Integration test (skipped if `dot` not found): `test_graphviz_render_roundtrip`
|
|
|
|
|
|
|
|
|
|
|
|
Deliverable: When `dot` is on PATH, graphviz diagrams render and embed; otherwise
|
|
|
|
|
|
source-only fallback.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## T04 — PlantUML integration
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: MRKD-WP-0005-T04
|
2026-03-17 12:16:09 +00:00
|
|
|
|
status: done
|
2026-03-16 12:55:34 +00:00
|
|
|
|
priority: medium
|
|
|
|
|
|
state_hub_task_id: ab0da6e4-a0f2-4a57-baac-5727b741c74f
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Implement the `PlantUMLRenderer` backend that shells out to `plantuml`.
|
|
|
|
|
|
|
|
|
|
|
|
- `PlantUMLRenderer.can_render("plantuml") -> bool`: checks `shutil.which("plantuml")`
|
|
|
|
|
|
- `PlantUMLRenderer.render(source, "plantuml", output_path)`:
|
|
|
|
|
|
- Write source to a temp `.puml` file
|
|
|
|
|
|
- Run `plantuml -tpng <input>` (output written adjacent to input by default)
|
|
|
|
|
|
- Move/rename output PNG to `output_path`
|
|
|
|
|
|
- On success / failure: same pattern
|
|
|
|
|
|
- Alt-text marker: `[plantuml-source]<source>`
|
|
|
|
|
|
- Add `plantuml` to optional extras: `diagram-plantuml`
|
|
|
|
|
|
- Integration test (skipped if `plantuml` not found): `test_plantuml_render_roundtrip`
|
|
|
|
|
|
|
|
|
|
|
|
Deliverable: When `plantuml` is on PATH, PlantUML diagrams render and embed.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## T05 — Rendered diagram round-trip integration tests
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: MRKD-WP-0005-T05
|
2026-03-17 12:16:09 +00:00
|
|
|
|
status: done
|
2026-03-16 12:55:34 +00:00
|
|
|
|
priority: medium
|
|
|
|
|
|
state_hub_task_id: 65b67ed9-5862-4322-acfb-5d39dab7e8d5
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Extend the regression corpus and test harness to cover the rendered-diagram path
|
|
|
|
|
|
end-to-end, in addition to the existing source-only tests.
|
|
|
|
|
|
|
|
|
|
|
|
- Add `tests/regression/level3/rendered_diagrams_document.md` with mermaid, graphviz,
|
|
|
|
|
|
and plantuml blocks
|
|
|
|
|
|
- `tests/regression/test_level3_rendered_diagrams.py`:
|
|
|
|
|
|
- Each test is `pytest.mark.skipif(shutil.which("mmdc") is None, ...)`
|
|
|
|
|
|
- Full round-trip: build → import → compare; assert diagrams preserved
|
|
|
|
|
|
- Assert `differ` reports zero broken diagrams when renderer was available
|
|
|
|
|
|
- Verify existing `test_level3_diagrams.py` (source-only) still passes regardless
|
|
|
|
|
|
of renderer availability — the fallback path must remain stable
|
|
|
|
|
|
|
|
|
|
|
|
Deliverable: Rendered-diagram regression tests pass on systems with renderers installed;
|
|
|
|
|
|
source-only tests pass on all systems.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## How to Work
|
|
|
|
|
|
|
|
|
|
|
|
- T01 must be complete before T02–T04 (abstraction must exist before concrete backends)
|
|
|
|
|
|
- T02, T03, T04 are independent of each other — can be worked in any order or in parallel
|
|
|
|
|
|
- T05 depends on T02–T04 (needs the renderers to test the rendered path)
|
|
|
|
|
|
- Use `pytest.mark.skipif` to gate renderer-dependent tests — never fail on missing tools
|
|
|
|
|
|
|
|
|
|
|
|
## Updating Task Status
|
|
|
|
|
|
|
|
|
|
|
|
```
|
2026-03-17 12:16:09 +00:00
|
|
|
|
status: done → status: in_progress (when you start it)
|
2026-03-16 12:55:34 +00:00
|
|
|
|
status: in_progress → status: done (when verified complete)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
When every task is `done`, set the frontmatter `status: done`.
|
|
|
|
|
|
|
|
|
|
|
|
## Success Criteria
|
|
|
|
|
|
|
|
|
|
|
|
Before marking the workplan done:
|
|
|
|
|
|
|
|
|
|
|
|
1. Every task block has `status: done`
|
|
|
|
|
|
2. Workplan frontmatter `status: done`
|
|
|
|
|
|
3. All existing tests pass unchanged (source-only fallback unbroken)
|
|
|
|
|
|
4. On a system with `mmdc`: `pytest tests/regression/test_level3_rendered_diagrams.py` passes
|
|
|
|
|
|
5. `ruff check` and `mypy` clean
|
|
|
|
|
|
6. Optional extras `diagram-mermaid`, `diagram-graphviz`, `diagram-plantuml` documented
|
|
|
|
|
|
in `pyproject.toml`
|