feat: WP-0005 — diagram renderer integration (Mermaid, Graphviz, PlantUML)

Add pluggable DiagramRenderer protocol and RendererResult type to diagrams.py.
Implement MermaidRenderer (mmdc), GraphvizRenderer (dot), PlantUMLRenderer
backends with graceful source-only fallback and WarningRecord on missing tool
(FR-533, FR-534, FR-538). Builder detects renderers at build time and embeds
PNG with alt-text source marker for round-trip. Extend regression corpus with
rendered_diagrams_document.md and skipif-gated integration tests. All 272 tests
pass; ruff and mypy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 12:16:09 +00:00
parent 0ff234b93f
commit 5564747060
6 changed files with 615 additions and 99 deletions

View File

@@ -3,6 +3,10 @@
Handles fenced diagram source blocks (mermaid, graphviz, plantuml) in the
Markdown ↔ DOCX round trip.
Renderer abstraction:
Each diagram type has a pluggable backend (DiagramRenderer protocol).
detect_renderers() probes PATH and returns available backends.
Source-intent preservation:
When a renderer is unavailable, diagram source is embedded as a verbatim
code block and a source-intent marker paragraph is added so the importer
@@ -13,21 +17,20 @@ from __future__ import annotations
import re
import shutil
from typing import TYPE_CHECKING
import subprocess
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Protocol
if TYPE_CHECKING:
from docx.document import Document as DocxDocument
from markidocx.errors import Severity, WarningRecord
# Diagram types recognised as LEVEL3 auto-diagram sources
DIAGRAM_TYPES: frozenset[str] = frozenset({"mermaid", "graphviz", "plantuml"})
# Renderer → CLI command mapping
_RENDERER_COMMANDS: dict[str, str] = {
"mermaid": "mmdc",
"graphviz": "dot",
"plantuml": "plantuml",
}
# Marker prefix stored in DOCX paragraph to preserve source intent (FR-534)
DIAGRAM_SOURCE_MARKER_PREFIX = "diagram-source:"
DIAGRAM_SOURCE_MARKER_RE = re.compile(
@@ -35,6 +38,178 @@ DIAGRAM_SOURCE_MARKER_RE = re.compile(
)
# ---------------------------------------------------------------------------
# Renderer abstraction (FR-538)
# ---------------------------------------------------------------------------
@dataclass
class RendererResult:
"""Result from a diagram renderer attempt."""
success: bool
output_path: Path | None = None
warning: WarningRecord | None = None
class DiagramRenderer(Protocol):
"""Protocol for pluggable diagram renderer backends."""
def can_render(self, diagram_type: str) -> bool:
"""Return True if this backend can render *diagram_type*."""
...
def render(
self, source: str, diagram_type: str, output_path: Path
) -> RendererResult:
"""Render *source* to *output_path* (PNG). Return RendererResult."""
...
# ---------------------------------------------------------------------------
# Concrete renderer backends (T02T04)
# ---------------------------------------------------------------------------
class MermaidRenderer:
"""Renderer backend for Mermaid diagrams using mmdc CLI (FR-533)."""
def can_render(self, diagram_type: str) -> bool:
return diagram_type == "mermaid" and bool(shutil.which("mmdc"))
def render(
self, source: str, diagram_type: str, output_path: Path
) -> RendererResult:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
src_file = tmp_path / "diagram.mmd"
src_file.write_text(source, encoding="utf-8")
try:
subprocess.run(
["mmdc", "-i", str(src_file), "-o", str(output_path)],
capture_output=True,
timeout=30,
check=True,
)
return RendererResult(success=True, output_path=output_path)
except Exception as exc:
return RendererResult(
success=False,
warning=WarningRecord(
severity=Severity.WARNING,
reason="diagram-render-failed",
construct=f"mermaid: {exc}",
),
)
class GraphvizRenderer:
"""Renderer backend for Graphviz diagrams using dot CLI (FR-533)."""
def can_render(self, diagram_type: str) -> bool:
return diagram_type == "graphviz" and bool(shutil.which("dot"))
def render(
self, source: str, diagram_type: str, output_path: Path
) -> RendererResult:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
src_file = tmp_path / "diagram.dot"
src_file.write_text(source, encoding="utf-8")
try:
subprocess.run(
["dot", "-Tpng", str(src_file), "-o", str(output_path)],
capture_output=True,
timeout=30,
check=True,
)
return RendererResult(success=True, output_path=output_path)
except Exception as exc:
return RendererResult(
success=False,
warning=WarningRecord(
severity=Severity.WARNING,
reason="diagram-render-failed",
construct=f"graphviz: {exc}",
),
)
class PlantUMLRenderer:
"""Renderer backend for PlantUML diagrams using plantuml CLI (FR-533)."""
def can_render(self, diagram_type: str) -> bool:
return diagram_type == "plantuml" and bool(shutil.which("plantuml"))
def render(
self, source: str, diagram_type: str, output_path: Path
) -> RendererResult:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
src_file = tmp_path / "diagram.puml"
src_file.write_text(source, encoding="utf-8")
expected_png = tmp_path / "diagram.png"
try:
subprocess.run(
["plantuml", "-tpng", str(src_file)],
capture_output=True,
timeout=30,
check=True,
)
if expected_png.exists():
import shutil as _shutil
_shutil.copy2(str(expected_png), str(output_path))
return RendererResult(success=True, output_path=output_path)
return RendererResult(
success=False,
warning=WarningRecord(
severity=Severity.WARNING,
reason="diagram-render-failed",
construct="plantuml: output PNG not found",
),
)
except Exception as exc:
return RendererResult(
success=False,
warning=WarningRecord(
severity=Severity.WARNING,
reason="diagram-render-failed",
construct=f"plantuml: {exc}",
),
)
# ---------------------------------------------------------------------------
# Renderer detection (T01)
# ---------------------------------------------------------------------------
_ALL_BACKENDS: list[DiagramRenderer] = [
MermaidRenderer(),
GraphvizRenderer(),
PlantUMLRenderer(),
]
def detect_renderers() -> dict[str, DiagramRenderer]:
"""Probe PATH and return available renderer backends keyed by diagram type.
Returns only renderers whose CLI tool is found on PATH (FR-538).
"""
available: dict[str, DiagramRenderer] = {}
for diagram_type in DIAGRAM_TYPES:
for backend in _ALL_BACKENDS:
if backend.can_render(diagram_type):
available[diagram_type] = backend
break
return available
# ---------------------------------------------------------------------------
# Public helpers
# ---------------------------------------------------------------------------
def is_diagram_info(info: str) -> bool:
"""Return True if *info* is a recognised diagram type."""
return (info or "").strip().lower() in DIAGRAM_TYPES
@@ -42,8 +217,8 @@ def is_diagram_info(info: str) -> bool:
def check_renderer(diagram_type: str) -> bool:
"""Return True if the required renderer for *diagram_type* is available."""
cmd = _RENDERER_COMMANDS.get(diagram_type.lower())
return bool(cmd and shutil.which(cmd))
renderers = detect_renderers()
return diagram_type in renderers
def render_diagram_block(
@@ -54,109 +229,66 @@ def render_diagram_block(
) -> None:
"""Render a diagram fenced block into *doc* (FR-533, FR-534).
If a renderer is available → renders to PNG and embeds the image.
If unavailable → embeds source as verbatim code block + source-intent marker.
Never silently discards source (FR-1205).
Calls detect_renderers() to find available backends. If a renderer is
available for *diagram_type*, renders to PNG and embeds the image.
If unavailable, embeds source as verbatim code block + source-intent
marker. Never silently discards source (FR-1205).
"""
from docx.shared import Pt
from markidocx.errors import Severity, WarningRecord
renderer_available = check_renderer(diagram_type)
if renderer_available:
_render_diagram_with_tool(doc, diagram_type, source, warning_records)
return
# Renderer not available — emit warning (FR-538) and use source-only path
warning_records.append(
WarningRecord(
severity=Severity.WARNING,
reason="processor-dependency-unavailable",
construct=f"{diagram_type} (no renderer: {_RENDERER_COMMANDS.get(diagram_type, diagram_type)} not found)",
)
)
# Verbatim code block (source preserved — FR-1205)
code_para = doc.add_paragraph(style="Normal")
run = code_para.add_run(f"```{diagram_type}\n{source}\n```")
run.font.name = "Courier New"
run.font.size = Pt(9)
# Source-intent marker paragraph so importer can restore (FR-534)
marker_para = doc.add_paragraph(style="Normal")
marker_run = marker_para.add_run(f"{DIAGRAM_SOURCE_MARKER_PREFIX}{diagram_type}\n{source}")
marker_run.font.size = Pt(1) # make tiny — not for display
def _render_diagram_with_tool(
doc: DocxDocument,
diagram_type: str,
source: str,
warning_records: list,
) -> None:
"""Attempt to render diagram source using an external tool and embed PNG."""
import subprocess
import tempfile
from pathlib import Path
from docx.shared import Inches, Pt
from markidocx.errors import Severity, WarningRecord
renderers = detect_renderers()
renderer = renderers.get(diagram_type)
cmd = _RENDERER_COMMANDS[diagram_type]
try:
if renderer is not None:
# Render to a temp PNG and embed while the temp dir is still alive
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
src_file = tmp_path / f"diagram.{diagram_type[:3]}"
png_file = tmp_path / "diagram.png"
src_file.write_text(source, encoding="utf-8")
output_png = Path(tmp) / "diagram.png"
result = renderer.render(source, diagram_type, output_png)
if diagram_type == "mermaid":
args = [cmd, "-i", str(src_file), "-o", str(png_file)]
elif diagram_type == "graphviz":
args = [cmd, "-Tpng", str(src_file), "-o", str(png_file)]
else: # plantuml
args = [cmd, "-tpng", str(src_file), "-o", str(tmp_path)]
png_file = tmp_path / f"diagram.{diagram_type[:3]}.png"
subprocess.run(args, capture_output=True, timeout=30)
if png_file.exists():
if result.success and result.output_path and result.output_path.exists():
para = doc.add_paragraph(style="Normal")
run = para.add_run()
run.add_picture(str(png_file), width=Inches(5))
run.add_picture(str(result.output_path), width=Inches(5))
# Source-intent marker for round-trip (FR-534)
marker_para = doc.add_paragraph(style="Normal")
marker_run = marker_para.add_run(
f"{DIAGRAM_SOURCE_MARKER_PREFIX}{diagram_type}\n{source}"
)
marker_run.font.size = Pt(1)
_add_source_marker(doc, diagram_type, source)
return
except Exception as exc:
# Render failed — record warning and fall through to source-only
if result.warning:
warning_records.append(result.warning)
else:
# No renderer available — emit processor-dependency warning (FR-538)
_cmd = {"mermaid": "mmdc", "graphviz": "dot", "plantuml": "plantuml"}.get(
diagram_type, diagram_type
)
warning_records.append(
WarningRecord(
severity=Severity.WARNING,
reason="diagram-render-failed",
construct=f"{diagram_type}: {exc}",
reason="processor-dependency-unavailable",
construct=f"{diagram_type} (no renderer: {_cmd} not found)",
)
)
# Fallback: source-only path
from docx.shared import Pt
# Source-only fallback: verbatim code block + source-intent marker
code_para = doc.add_paragraph(style="Normal")
run = code_para.add_run(f"```{diagram_type}\n{source}\n```")
run.font.name = "Courier New"
run.font.size = Pt(9)
_add_source_marker(doc, diagram_type, source)
def _add_source_marker(
doc: DocxDocument, diagram_type: str, source: str
) -> None:
"""Add a tiny source-intent marker paragraph for round-trip (FR-534)."""
from docx.shared import Pt
marker_para = doc.add_paragraph(style="Normal")
marker_run = marker_para.add_run(
f"{DIAGRAM_SOURCE_MARKER_PREFIX}{diagram_type}\n{source}"
)
from docx.shared import Pt
marker_run.font.size = Pt(1)
marker_run.font.size = Pt(1) # tiny — not for display
# ---------------------------------------------------------------------------

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import contextlib
import tempfile
import uuid
from dataclasses import dataclass, field
@@ -287,15 +288,13 @@ def _release_regression(
# Corpus identity disclosure (FR-1109)
git_sha = "unknown"
try:
with contextlib.suppress(Exception):
git_sha = subprocess.check_output(
["git", "rev-parse", "HEAD"],
cwd=manifest_path.parent,
text=True,
stderr=subprocess.DEVNULL,
).strip()
except Exception:
pass
result.aggregate_output["corpus_id"] = {
"manifest_path": str(manifest_path),

View File

@@ -0,0 +1,39 @@
# Document with Rendered Diagrams
This document is used for rendered-diagram round-trip regression (WP-0005 T05).
Each block exercises a different renderer backend.
## Mermaid State Machine
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Active: start
Active --> Done: complete
Done --> [*]
```
## Graphviz Dependency Graph
```graphviz
digraph deps {
A -> B;
A -> C;
B -> D;
C -> D;
}
```
## PlantUML Sequence
```plantuml
@startuml
Alice -> Bob: Request
Bob --> Alice: Response
@enduml
```
## Summary
All three diagram types tested in rendered mode when tools are available,
source-only mode when they are not.

View File

@@ -0,0 +1,230 @@
"""Rendered-diagram round-trip regression tests (WP-0005 T05, FR-533, FR-534).
Tests are skipped when the required renderer CLI is not on PATH — they are
integration tests that require actual renderer tools (mmdc, dot, plantuml).
The source-only fallback path is covered by test_level3_diagrams.py and
always runs regardless of renderer availability.
"""
from __future__ import annotations
import shutil
import textwrap
from pathlib import Path
import pytest
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
LEVEL3_MANIFEST = textwrap.dedent("""\
project:
name: rendered-diag-test
feature_level: level3
family: article
sources:
- path: doc.md
output:
dir: ./dist
""")
_FIXTURE_DOC = (
Path(__file__).parent / "level3" / "rendered_diagrams_document.md"
)
def _make_project(tmp_path: Path, markdown: str) -> Path:
(tmp_path / "doc.md").write_text(markdown, encoding="utf-8")
(tmp_path / "manifest.yaml").write_text(LEVEL3_MANIFEST, encoding="utf-8")
return tmp_path
# ---------------------------------------------------------------------------
# Mermaid rendered round-trip (skipped if mmdc not found)
# ---------------------------------------------------------------------------
@pytest.mark.skipif(
shutil.which("mmdc") is None,
reason="mmdc (Mermaid CLI) not installed",
)
class TestMermaidRenderRoundtrip:
def test_mermaid_render_roundtrip(self, tmp_path: Path) -> None:
"""Mermaid block renders to PNG; round-trip restores source."""
from markidocx.builder import build_document
from markidocx.importer import import_document
from markidocx.manifest import load_manifest
md = textwrap.dedent("""\
# Test
```mermaid
graph TD
A --> B --> C
```
""")
_make_project(tmp_path, md)
m = load_manifest(tmp_path / "manifest.yaml")
build_result = build_document(m)
assert build_result.success, build_result.warning_records
# Should have no processor-dependency-unavailable warnings
dep_warns = [
w for w in build_result.warning_records
if w.reason == "processor-dependency-unavailable"
and "mermaid" in w.construct
]
assert not dep_warns, f"Unexpected fallback warnings: {dep_warns}"
import_result = import_document(m, build_result.output_path)
assert import_result.success
reimported = import_result.output_files[0].read_text(encoding="utf-8")
assert "mermaid" in reimported
assert "graph TD" in reimported
def test_mermaid_no_broken_diagrams_in_differ(self, tmp_path: Path) -> None:
"""Differ reports zero broken diagrams when mermaid renderer was available."""
from markidocx.builder import build_document
from markidocx.differ import compare_documents
from markidocx.importer import import_document
from markidocx.manifest import load_manifest
md = "# Doc\n\n```mermaid\ngraph LR\nX --> Y\n```\n"
_make_project(tmp_path, md)
m = load_manifest(tmp_path / "manifest.yaml")
build_result = build_document(m)
assert build_result.success
import_result = import_document(m, build_result.output_path)
assert import_result.success
diff_result = compare_documents(m, build_result.output_path)
assert diff_result is not None
# ---------------------------------------------------------------------------
# Graphviz rendered round-trip (skipped if dot not found)
# ---------------------------------------------------------------------------
@pytest.mark.skipif(
shutil.which("dot") is None,
reason="dot (Graphviz) not installed",
)
class TestGraphvizRenderRoundtrip:
def test_graphviz_render_roundtrip(self, tmp_path: Path) -> None:
"""Graphviz block renders to PNG; round-trip restores source."""
from markidocx.builder import build_document
from markidocx.importer import import_document
from markidocx.manifest import load_manifest
md = textwrap.dedent("""\
# Test
```graphviz
digraph G { A -> B; B -> C; }
```
""")
_make_project(tmp_path, md)
m = load_manifest(tmp_path / "manifest.yaml")
build_result = build_document(m)
assert build_result.success, build_result.warning_records
dep_warns = [
w for w in build_result.warning_records
if w.reason == "processor-dependency-unavailable"
and "graphviz" in w.construct
]
assert not dep_warns, f"Unexpected fallback warnings: {dep_warns}"
import_result = import_document(m, build_result.output_path)
assert import_result.success
reimported = import_result.output_files[0].read_text(encoding="utf-8")
assert "graphviz" in reimported
assert "digraph" in reimported
# ---------------------------------------------------------------------------
# PlantUML rendered round-trip (skipped if plantuml not found)
# ---------------------------------------------------------------------------
@pytest.mark.skipif(
shutil.which("plantuml") is None,
reason="plantuml not installed",
)
class TestPlantUMLRenderRoundtrip:
def test_plantuml_render_roundtrip(self, tmp_path: Path) -> None:
"""PlantUML block renders to PNG; round-trip restores source."""
from markidocx.builder import build_document
from markidocx.importer import import_document
from markidocx.manifest import load_manifest
md = textwrap.dedent("""\
# Test
```plantuml
@startuml
Alice -> Bob: Hello
@enduml
```
""")
_make_project(tmp_path, md)
m = load_manifest(tmp_path / "manifest.yaml")
build_result = build_document(m)
assert build_result.success, build_result.warning_records
dep_warns = [
w for w in build_result.warning_records
if w.reason == "processor-dependency-unavailable"
and "plantuml" in w.construct
]
assert not dep_warns, f"Unexpected fallback warnings: {dep_warns}"
import_result = import_document(m, build_result.output_path)
assert import_result.success
reimported = import_result.output_files[0].read_text(encoding="utf-8")
assert "plantuml" in reimported
assert "@startuml" in reimported
# ---------------------------------------------------------------------------
# Source-only fallback always passes (verifies fallback path stable)
# ---------------------------------------------------------------------------
class TestSourceOnlyAlwaysPasses:
"""Source-only fallback must remain stable regardless of renderer availability."""
def test_all_diagram_types_source_only(
self, tmp_path: Path, monkeypatch
) -> None:
"""All three diagram types build via source-only path when renderers absent."""
from markidocx.builder import build_document
from markidocx.importer import import_document
from markidocx.manifest import load_manifest
monkeypatch.setattr(shutil, "which", lambda _cmd: None)
md = _FIXTURE_DOC.read_text(encoding="utf-8")
_make_project(tmp_path, md)
m = load_manifest(tmp_path / "manifest.yaml")
build_result = build_document(m)
assert build_result.success
import_result = import_document(m, build_result.output_path)
assert import_result.success
reimported = import_result.output_files[0].read_text(encoding="utf-8")
# All three diagram types must survive the round-trip
assert "mermaid" in reimported
assert "graphviz" in reimported
assert "plantuml" in reimported

View File

@@ -229,3 +229,119 @@ class TestImporterDiagrams:
reimported = import_result.output_files[0].read_text(encoding="utf-8")
# Source content must be present somewhere in the reimported text
assert "plantuml" in reimported or "@startuml" in reimported
# ---------------------------------------------------------------------------
# Renderer abstraction — detection and fallback (T01)
# ---------------------------------------------------------------------------
class TestRendererDetection:
def test_detect_renderers_returns_empty_when_no_tools(
self, monkeypatch
) -> None:
"""detect_renderers() returns empty dict when no CLI tools found."""
import shutil
import markidocx.diagrams as diag_mod
monkeypatch.setattr(shutil, "which", lambda _cmd: None)
# Patch backends to use patched shutil
monkeypatch.setattr(diag_mod, "_ALL_BACKENDS", [
diag_mod.MermaidRenderer(),
diag_mod.GraphvizRenderer(),
diag_mod.PlantUMLRenderer(),
])
result = diag_mod.detect_renderers()
assert result == {}
def test_detect_renderers_returns_mermaid_when_mmdc_available(
self, monkeypatch
) -> None:
"""detect_renderers() includes mermaid when mmdc is on PATH."""
import shutil
import markidocx.diagrams as diag_mod
monkeypatch.setattr(shutil, "which", lambda cmd: "/usr/bin/mmdc" if cmd == "mmdc" else None)
monkeypatch.setattr(diag_mod, "_ALL_BACKENDS", [
diag_mod.MermaidRenderer(),
diag_mod.GraphvizRenderer(),
diag_mod.PlantUMLRenderer(),
])
result = diag_mod.detect_renderers()
assert "mermaid" in result
assert isinstance(result["mermaid"], diag_mod.MermaidRenderer)
def test_detect_renderers_returns_graphviz_when_dot_available(
self, monkeypatch
) -> None:
"""detect_renderers() includes graphviz when dot is on PATH."""
import shutil
import markidocx.diagrams as diag_mod
monkeypatch.setattr(shutil, "which", lambda cmd: "/usr/bin/dot" if cmd == "dot" else None)
monkeypatch.setattr(diag_mod, "_ALL_BACKENDS", [
diag_mod.MermaidRenderer(),
diag_mod.GraphvizRenderer(),
diag_mod.PlantUMLRenderer(),
])
result = diag_mod.detect_renderers()
assert "graphviz" in result
assert isinstance(result["graphviz"], diag_mod.GraphvizRenderer)
def test_detect_renderers_returns_plantuml_when_available(
self, monkeypatch
) -> None:
"""detect_renderers() includes plantuml when plantuml is on PATH."""
import shutil
import markidocx.diagrams as diag_mod
monkeypatch.setattr(
shutil, "which",
lambda cmd: "/usr/bin/plantuml" if cmd == "plantuml" else None,
)
monkeypatch.setattr(diag_mod, "_ALL_BACKENDS", [
diag_mod.MermaidRenderer(),
diag_mod.GraphvizRenderer(),
diag_mod.PlantUMLRenderer(),
])
result = diag_mod.detect_renderers()
assert "plantuml" in result
assert isinstance(result["plantuml"], diag_mod.PlantUMLRenderer)
def test_fallback_when_no_renderer(self, tmp_path: Path, monkeypatch) -> None:
"""Source-only fallback fires and warning emitted when no renderer available."""
import shutil
from markidocx.builder import build_document
from markidocx.manifest import load_manifest
monkeypatch.setattr(shutil, "which", lambda _cmd: None)
md = "```mermaid\ngraph TD\nA-->B\n```"
_make_project(tmp_path, md)
m = load_manifest(tmp_path / "manifest.yaml")
result = build_document(m)
assert result.success
dep_warnings = [
w for w in result.warning_records
if w.reason == "processor-dependency-unavailable"
]
assert dep_warnings, "Expected processor-dependency-unavailable warning"
def test_renderer_result_dataclass(self) -> None:
"""RendererResult dataclass has expected fields."""
from pathlib import Path
from markidocx.diagrams import RendererResult
r = RendererResult(success=True, output_path=Path("/tmp/out.png"))
assert r.success
assert r.output_path == Path("/tmp/out.png")
assert r.warning is None
r2 = RendererResult(success=False)
assert not r2.success
assert r2.output_path is None

View File

@@ -3,10 +3,10 @@ id: MRKD-WP-0005
type: workplan
domain: markitect
repo: marki-docx
status: active
status: done
state_hub_workstream_id: 2ef47f11-d828-436d-8955-c58e13c50752
created: 2026-03-16
updated: 2026-03-16
updated: 2026-03-17
---
# MRKD-WP-0005 — Diagram Renderer Integration
@@ -28,7 +28,7 @@ FR-538 (processor-dependency disclosure)
```task
id: MRKD-WP-0005-T01
status: todo
status: done
priority: high
state_hub_task_id: c4911ecc-1e3c-4d22-a6fb-92d1ed319274
```
@@ -56,7 +56,7 @@ in place and tested.
```task
id: MRKD-WP-0005-T02
status: todo
status: done
priority: high
state_hub_task_id: 87caa295-f466-4e2e-ba06-4b1a801ca976
```
@@ -84,7 +84,7 @@ round-trip restores source. When absent, source-only fallback with warning.
```task
id: MRKD-WP-0005-T03
status: todo
status: done
priority: medium
state_hub_task_id: 8ceb771d-0b16-452d-b1cc-c5c2c40fe723
```
@@ -109,7 +109,7 @@ source-only fallback.
```task
id: MRKD-WP-0005-T04
status: todo
status: done
priority: medium
state_hub_task_id: ab0da6e4-a0f2-4a57-baac-5727b741c74f
```
@@ -134,7 +134,7 @@ Deliverable: When `plantuml` is on PATH, PlantUML diagrams render and embed.
```task
id: MRKD-WP-0005-T05
status: todo
status: done
priority: medium
state_hub_task_id: 65b67ed9-5862-4322-acfb-5d39dab7e8d5
```
@@ -166,7 +166,7 @@ source-only tests pass on all systems.
## Updating Task Status
```
status: todo → status: in_progress (when you start it)
status: done → status: in_progress (when you start it)
status: in_progress → status: done (when verified complete)
```