fix(130): enforce mmdc as hard prerequisite with loud preflight/mid-render aborts#148
Merged
davidmatousek merged 8 commits intomainfrom Apr 11, 2026
Merged
fix(130): enforce mmdc as hard prerequisite with loud preflight/mid-render aborts#148davidmatousek merged 8 commits intomainfrom
davidmatousek merged 8 commits intomainfrom
Conversation
Feature 130 fixes silent failure in attack path Mermaid rendering when mmdc is not installed. This commit lays the governance groundwork for the fix: PRD, spec, plan, research, tasks, agent assignments, and a new ADR establishing mmdc as a hard prerequisite. - PRD 130: Fix Attack Path Mermaid Rendering (PM/Architect/Team-Lead approved) - ADR-022: First ADR governing CLI-prerequisite posture in tachi. Decision: mmdc is a hard prerequisite when attack-trees/ contains Critical/High findings. Cross-refs ADR-014 (optional external APIs) and ADR-021 (determinism). Future Work clause defers install.sh helper extraction until a 3rd CLI prereq arrives. - Spec/plan/tasks/research/agent-assignments under specs/130-prd-130-fix/ - docs/architecture/01_system_design/README.md: Feature 130 components section covering preflight gate (shell + Python), mid-render aggregator, CI workflow, and docs sync cluster. - docs/product/02_PRD/INDEX.md: add row 130 - docs/product/_backlog/BACKLOG.md: regenerate after Issue #130 moved to Build stage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Turn silent failure into loud failure when mmdc is missing. Previously
the pipeline silently fell back to raw Mermaid text in the PDF when
mmdc was unavailable; now the pipeline aborts at preflight with a
canonical three-line install message and non-zero exit.
User Story 1 (T004-T008) — prerequisites are enforced, not assumed:
- .claude/commands/tachi.security-report.md Step 2: shell-level gate.
Detects attack-trees/*.md presence, then `command -v mmdc`. If mmdc
is missing, echoes canonical message to stderr and halts non-zero.
Mirrors the existing Typst check. Skip the check on projects without
attack trees — they do not need mmdc.
- scripts/extract-report-data.py render_mermaid_to_png(): replace the
silent shutil.which("mmdc") warn+fallback with raise RuntimeError
using the canonical message. Defense-in-depth for direct Python
invocations (tests, tooling).
Also: add `sys.path.insert(0, ...)` before the tachi_parsers import,
parallel to extract-infographic-data.py, required enabler for the
importlib-based test fixture.
- templates/tachi/security-report/attack-path.typ: delete the
`else if mermaid-text != ""` branch entirely. With the preflight
gate guaranteeing mmdc presence, the text-fallback branch is dead
code. The `if has-img and img-path != ""` branch is now the only
render path.
- tests/scripts/test_mmdc_preflight.py (new): 4 preflight tests +
5 mid-render aggregator tests. The 4 preflight tests pass; the
5 mid-render tests fail as expected (Wave 4 T010/T011 will land
the module-level _render_single + failure aggregator to make
them green).
Verification:
- 4/4 preflight tests green under test_mmdc_preflight.py -k preflight
- 5/5 backward-compatibility baselines still byte-identical under
SOURCE_DATE_EPOCH=1700000000 (happy path unchanged)
- Canonical three-line error message contains: @mermaid-js/mermaid-cli,
`npm install -g @mermaid-js/mermaid-cli`, and "Attack path rendering"
Tasks: T001-T008 marked [X] in tasks.md. Waves 1-3 complete (8/32);
Wave 4 (US2 mid-render aggregator) next.
Refs: ADR-022, spec.md FR-130.1/FR-130.3, plan.md Phase 1 Design
R5-R7 refinements
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User Story 2 (T009-T012) — mid-render failures are loud, not silent.
When mmdc is installed but a specific attack tree fails to render
(syntax error, crash, timeout), the pipeline previously set
has_image=False and silently produced a PDF with some pages missing.
Now any render failure aborts the pipeline with an informative per-
finding error list on stderr.
Implementation:
- scripts/extract-report-data.py: promote _render_single from nested
closure to module-level function so tests can patch.object on it.
Extend the return shape to (entry, success, value) where on failure
`value` is a structured error record dict:
- id: finding ID
- file_path: canonical "attack-trees/{fid_lower}.mmd" path
- failure_class: "exit:<code>", "timeout", or "signal"
- stderr_excerpt: first 200 bytes of stderr (utf-8, errors=replace)
- render_mermaid_to_png() as_completed loop: collect failures into a
list. When the loop ends, if the failure list is non-empty, format
per R6 spec and raise RuntimeError:
Attack path rendering failed for N findings:
- F-002 (attack-trees/f-002.mmd)
failure: exit:2
stderr: Parse error on line 5
R6 (the highest-priority architect refinement) is the format that
decides whether this feature delivers on its fail-loud promise.
- _render_single uses two module-level globals (_render_attack_trees_dir
and _render_rel_target) published by render_mermaid_to_png before
pool execution. This keeps _render_single's signature at 2 positional
args (entry, tmp_path) — required by the test harness's 2-arg mock.
Thread-safe for the sequential CLI model (documented inline).
- No new imports. No new dependencies. Runtime stdlib-only constraint
preserved per ADR-001 / Constitution Principle III.
Verification:
- 9/9 tests pass under test_mmdc_preflight.py (4 preflight + 5 mid-render)
- 5/5 backward-compatibility baselines still byte-identical under
SOURCE_DATE_EPOCH=1700000000 — happy path is unchanged
- R6 format assertions verified:
* summary line "Attack path rendering failed for N findings:"
* per-finding ID + file path + failure class + stderr excerpt
- R7 distinction verified: mid-render message does NOT contain
"npm install -g @mermaid-js/mermaid-cli" (preflight's distinct shape)
Tasks: T009-T012 marked [X]. Wave 4 complete (12/32, 37.5%).
Wave 5 (US3 docs sync, T013-T017) next.
Refs: spec.md FR-130.3/FR-130.4, plan.md R5/R6/R7 refinements,
ADR-022
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…h, Tech Stack, spec 112 User Story 3 (T013-T017) — document the mmdc hard-prerequisite posture consistently across all user-facing and spec-level documentation. Changes: - README.md: new Prerequisites section between "What is tachi?" and "Quick Start". Names typst and @mermaid-js/mermaid-cli as required external CLIs with macOS/Linux/WSL install commands. Cross-links to ADR-022 for rationale. - scripts/install.sh: courtesy warning block if mmdc is missing. Points users to README Prerequisites and the canonical install command. Does NOT check Typst (scope containment per plan S2 decision — the per-command preflight is the enforcement, install.sh warning is advisory only). - docs/architecture/00_Tech_Stack/README.md: update mmdc entry (line 272) from "optional with graceful fallback" to "hard prerequisite as of Feature 130 — see ADR-022". Rewrite the surrounding note (line 279) to describe preflight enforcement with cross-link to ADR-022. Makes ADR-022 discoverable from Tech Stack per plan Risk #5. - specs/112-attack-path-pages/spec.md: invert SC-004. Was "When the rendering tool is unavailable, 100% of attack path pages still appear with text fallback." Now "Rendering tool availability is verified at preflight; pipeline aborts loudly if unavailable." Adds audit-trail comment above the inverted criterion: <!-- Inverted by Feature 130 (2026-04-11): text fallback is no longer a supported shipping mode --> Also rewrites the Assumptions bullet that previously said mmdc is not a hard dependency. - specs/112-attack-path-pages/research.md: correct the pymmdc factual error — pymmdc on PyPI is a GPL-3.0 Python wrapper around the Node.js @mermaid-js/mermaid-cli CLI, not a pure-Python renderer. Adds a "Durable Decision Rationale" block documenting the mmdc-hard-prereq choice with references to Feature 130 PRD Rejected Alternatives (A-E) and ADR-022. Verification: - All 14 tests still green (9 preflight + 5 baselines) - Canonical install command `npm install -g @mermaid-js/mermaid-cli` appears verbatim in all 5 edited files - 8 ADR-022 references across 4 files (Tech Stack + spec/research 112 + README) - Audit-trail comment present in spec 112 with exact date 2026-04-11 Tasks: T013-T017 marked [X]. Wave 5 complete (17/32, 53%). Wave 6 (Cross-Cutting & CI, T018-T029) next. Refs: spec.md FR-130.5/FR-130.6, ADR-022, plan Risk #5 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wave 6 of Feature 130 — cross-cutting artifacts and verification gates. CI workflow (T018, FR-130.7): - .github/workflows/tachi-mmdc-preflight.yml on ubuntu-latest - Installs typst-community/setup-typst@v5 + Python 3.11; does NOT install mmdc - Diagnostic echo (R3) + enforcement assertion (team-lead T4, plan Risk #6) — fails workflow if mmdc unexpectedly appears on PATH - Invokes python3 scripts/extract-report-data.py directly (no slash-command agent orchestration in CI); asserts non-zero exit + all 3 canonical tokens (@mermaid-js/mermaid-cli, npm install -g @mermaid-js/mermaid-cli, Attack path rendering) grep-asserted individually Docs (T025, T026): - CLAUDE.md Recent Changes: prepend Feature 130 entry above Feature 136 - specs/130-prd-130-fix/quickstart.md: developer reproduction, loud-failure validation, happy-path baseline run, regeneration instructions Verification (T019-T024, T027-T031 automation): - T019: BASELINE_EXAMPLES confirmed to exclude agentic-app/sample-report/ (R8 negative — Feature 128 decision stands) - T020/T021: mermaid-agentic-app baseline and agentic-app/sample-report PDFs regenerated under SOURCE_DATE_EPOCH=1700000000; byte-identical to prior - T022: backward-compat 5/5 baselines byte-identical vs T002 pre-flight snapshot (R9 before/after guardrail pair complete) - T023: canonical install command appears in exactly the 7 required enforcement locations (R4) - T024: dead-code greps return zero — else-if-mermaid-text branch deleted in attack-path.typ, silent-fallback has_image=False loop replaced with raise RuntimeError in extract-report-data.py shutil.which context - T027/T028/T029 automated: pipeline exit 0 with mmdc present (byte-ident to baseline, 25 image xobjects); pipeline exit 1 with clean PATH emitting all 3 canonical tokens; 47-tree sample-report embeds 39 image xobjects - T030: full pytest 48/48 pass - T031: constitutional walk-through — no runtime deps added, stdlib-only preserved, feature branch workflow intact, conventional commits Tasks.md: T018-T031 marked [X]. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wave 7 polish — code-reviewer non-blocking fixes and PR assembly. - templates/tachi/security-report/attack-path.typ: remove stale "image or text fallback" references in header comment (line 4-5) and attack-path-page parameter docstring (line 38-39). The `mermaid-text` field and text-fallback branch were deleted in T007; comments now reflect that only the rendered-image path exists. Pure comment change — backward-compat baselines verified byte-identical post-fix. - specs/130-prd-130-fix/PR-description.md: T032 PR body for /aod.deliver. Assembles FR-130.1 through FR-130.7 deliverables with commit SHAs, Before/After CHANGELOG narrative, test coverage summary, automated manual-validation results (T027/T028/T029), governance links (ADR-022, spec 112 corrections, Tech Stack doc), and reviewer checklist. - specs/130-prd-130-fix/tasks.md: T032 marked [X]. All 32 tasks complete. - docs/product/_backlog/BACKLOG.md: timestamp bump from /aod.build Step 1 GitHub stage label update (issue #130 to stage:build). Full suite 48/48 pass. Backward-compat 5/5 baselines byte-identical under SOURCE_DATE_EPOCH=1700000000. Feature ready for Step 6 security scan. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SAST: 3 files scanned (extract-report-data.py, install.sh, test_mmdc_preflight.py). OWASP P0 pattern review clean — no injection, no hardcoded secrets, no path traversal introduced, no weak crypto, no auth/session handling. SCA: skipped — no dependency manifests changed on this feature branch. Status: PASSED, 0 findings across all severity tiers. Scan ID: 65936668-f62b-4548-a9bd-e503c6e114ff Chain hash: 0bf47a425fcfddef942052831cb8ea9e25a3629d3b9529a45383a4136d844892 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
T030: full pytest suite 48/48 green T031: conventional commits, stdlib-only, branch hygiene verified Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Feature 130 (PRD #130) turns a silent pipeline failure into a loud, actionable one: when
@mermaid-js/mermaid-cli(mmdc) is absent on a shell running/tachi.security-reportagainst a project with Critical/High attack trees, the pipeline now aborts at preflight with a canonical three-line install command instead of silently shipping a PDF containing 40+ lines of rawflowchart TDsource on every attack-path page. Defense-in-depth is enforced at two entry points (shell-level in the command file, Python-level inscripts/extract-report-data.py), the unreachable text-fallback branch intemplates/tachi/security-report/attack-path.typis deleted outright, mid-render failures (mmdc present but a specific tree fails) now abort with a per-finding error list, andmmdcis documented as a hard prerequisite across README,install.sh, the Tech Stack doc, and parent spec 112. The change is governed by the first ADR in tachi covering CLI-prerequisite posture: ADR-022.Before / After
Before. When
mmdcwas absent fromPATHand the target project had attack trees underattack-trees/,scripts/extract-report-data.py::render_mermaid_to_png()hit a silentshutil.which("mmdc")check that printed a one-line warning to stderr, flipped every attack-tree entry'shas_imagefield toFalse, and continued. The Typst template then followed anelse if mermaid-text != ""fallback branch inattack-path.typthat emitted the raw Mermaid source as arawcode block inside the PDF. The pipeline reported exit 0. The CI workflow reported success. The only way a user discovered the broken output was by flipping through the PDF and seeingflowchart TDtext where a rendered attack tree should have been. The same silent-failure shape held for the rarer "mmdc installed but render crashed" case: each failing entry was markedhas_image=Falseand the same text fallback kicked in. Both modes violated spec 112 SC-003 (legibility at page size) and directly blocked the flagship "show to exec board" deliverable.After. The pipeline aborts at preflight with the canonical three-line error message whenever
mmdcis missing and attack trees are present. Both error paths emit an identical message containing@mermaid-js/mermaid-cli,npm install -g @mermaid-js/mermaid-cli, andAttack path rendering:Defense-in-depth is implemented as two gates: a shell-level
command -v mmdcin.claude/commands/tachi.security-report.mdStep 1 (mirroring the existing Typst check) and a Python-levelshutil.which("mmdc")insiderender_mermaid_to_png()that raisesRuntimeErrorinstead of warning silently. Both checks fire only whenattack-trees/*.mdcontains at least one file with Critical/High severity — projects without attack trees continue to work withoutmmdc. Mid-render failures (a specific tree failing after mmdc has passed preflight) are collected by a new per-finding aggregator that raisesRuntimeError("Attack path rendering failed for N findings: ...")with each failed finding's ID, file path, failure class (exit:<code>,timeout,signal), and a 200-byte stderr excerpt (architect refinement R6). The text-fallback branch atattack-path.typ:78-86is deleted outright — no placeholder, no comment, no "removed in 130" stub. The only render path that remains is the PNG-image branch. Release-please will auto-cut a new tachi release on merge.FR-130.x Deliverables
mmdcabsent and attack trees presentdb0073c.claude/commands/tachi.security-report.md,scripts/extract-report-data.pyif not shutil.which("mmdc"):fallback inrender_mermaid_to_png(); convert toraise RuntimeErrordb0073cscripts/extract-report-data.pylines 721-730else if mermaid-text != ""text-fallback branch outrightdb0073ctemplates/tachi/security-report/attack-path.typlines 78-86mmdcsubprocess failure732fd49scripts/extract-report-data.py_render_single+as_completedloopinstall.shmmdc check, Tech Stack doc line 279, spec 112 SC-004 inversion, spec 112 research.md pymmdc correctionb46e931README.md,scripts/install.sh,docs/architecture/00_Tech_Stack/README.md,specs/112-attack-path-pages/spec.md,specs/112-attack-path-pages/research.mdmermaid-agentic-appunderSOURCE_DATE_EPOCH=1700000000;agentic-app/sample-reportnon-baseline regeneration to confirm 47-tree rendering648b4d1examples/mermaid-agentic-app/security-report.pdf.baseline,examples/agentic-app/sample-report/security-report.pdfubuntu-latest(no mmdc preinstalled) that runs the pipeline againstexamples/mermaid-agentic-app/and asserts non-zero exit with all 3 canonical tokens in stderr648b4d1.github/workflows/tachi-mmdc-preflight.ymlCommit lineage:
528204fscaffolded the feature workspace (PRD, spec, plan, research, ADR-022, agent-assignments), then the four implementation commits landed in topological order (US1 preflight → US2 mid-render → US3 docs → Phase 6 CI and verification).Test Coverage
tests/scripts/test_mmdc_preflight.py(new file, 339 lines, 9 pytest cases):test_preflight_raises_when_mmdc_missing,test_preflight_skipped_when_attack_trees_empty,test_preflight_skipped_when_only_low_medium_findings,test_preflight_error_distinct_from_midrendertest_midrender_aggregator_raises_on_any_failure,test_midrender_aggregator_message_format,test_midrender_all_success_no_exception,test_midrender_all_failure_raises_with_full_list,test_midrender_error_distinct_from_preflighttests/scripts/test_backward_compatibility.py: 5/5 baselines byte-identical underSOURCE_DATE_EPOCH=1700000000forweb-app,microservices,ascii-web-api,mermaid-agentic-app,free-text-microservice.agentic-app/sample-report/remains intentionally excluded fromBASELINE_EXAMPLESper Feature 128 decision (architect refinement R8 explicit negative task T019 verified).pytest tests/returns 48/48 pass (T030 Wave 7 verification gate). No skipped tests without documented justification..github/workflows/tachi-mmdc-preflight.ymlruns onubuntu-latest(which ships without mmdc preinstalled), installs Typst and Python 3.11, omits the mmdc install step, then runs the pipeline againstexamples/mermaid-agentic-app/and asserts exit code non-zero with all 3 canonical tokens in stderr. Includes awhich mmdc || echo "expected absence"diagnostic step (architect refinement R3) and a team-lead T4 enforcement assertion that fails the job if mmdc is unexpectedly present on PATH (plan Risk feat(005): implement STRIDE threat agents #6 mitigation).Manual Validation Results (T027 / T028 / T029)
Full evidence at
.aod/results/tester-130-t027-t028-t029.md. Summary:examples/mermaid-agentic-app/on a shell withmmdcat/opt/homebrew/bin/mmdc. Pipeline exit 0; 12 Critical/High attack trees extracted; output PDF byte-identical to the committed baseline (zero-line xxd diff); 25 image xobjects embedded. Raw Mermaid source is now structurally impossible in the output — T024 confirmed theelse if mermaid-textbranch is deleted, so the only render path is the image branch.env -i PATH="/usr/bin:/bin" /usr/bin/python3 scripts/extract-report-data.py --target-dir examples/mermaid-agentic-app/ ...(clean PATH, mmdc absent). Pipeline exit 1.RuntimeErrormessage contains all 3 canonical tokens. No PDF produced at any output path. Error message format matches FR-130.1 spec exactly.examples/agentic-app/sample-report/security-report.pdf(6.4MB, ~10s) with 17 Critical/High trees dispatched, zero mmdc/Typst warnings; regenerated PDF embeds 39 image xobjects (higher than mermaid-agentic-app's 25, proportional to tree count 47 vs 24).Visual PDF inspection is deferred to the PR reviewer as a belt-and-suspenders courtesy check — the automation above provides dispositive evidence that rendering works end-to-end.
Before/after backward-compatibility guardrail (architect refinement R9, High priority):
.aod/results/130-baseline-pretest.md— 5 passed in 11.71s, all 5 baselines byte-identical, captured before any Feature 130 code changes.aod/results/130-baseline-posttest.md— 5 passed in 8.79s, all 5 baselines byte-identical, captured after b46e931 (preflight gate + mid-render aggregator + docs landed)Governance Artifacts
mmdcas a hard prerequisite whenattack-trees/contains at least one Critical/High finding, the defense-in-depth two-gate rationale (parity with Feature 054 Typst precedent), the explicit rejection of pymmdc/Kroki/auto-install alternatives, a Future Work clause on extracting a reusable helper once a third CLI prerequisite is added, and cross-references to ADR-014 (optional external APIs — different class of dependency) and ADR-021 (determinism convention used by the baseline regeneration in T020/T022). The ADR notes that the canonical install command appears in exactly 7 locations across the codebase, verified by the T023 grep pass.specs/112-attack-path-pages/spec.md,research.md):<!-- Inverted by Feature 130 (2026-04-11): text fallback is no longer a supported shipping mode -->added above the new SC-004.research.mdline 80 corrected: pymmdc is not a pure-Python renderer — it is a GPL-3.0 licensed Node.js wrapper around the same@mermaid-js/mermaid-cliCLI, license-incompatible with tachi's distribution model.research.mdlines 91-93: Durable Decision Rationale block added documenting the mmdc-hard-prerequisite choice with references to Feature 130 and the PRD Rejected Alternatives section (A through E).docs/architecture/00_Tech_Stack/README.mdline 279): prior "optional with text fallback" sentence replaced by "hard prerequisite whenattack-trees/contains Critical/High findings, aborting at preflight if missing." Cross-linked to ADR-022 (plan Risk STRIDE Threat Agents #5 mitigation — ensures ADR-022 is discoverable from the Tech Stack doc).Risk Register — What Did NOT Regress
pytest tests/scripts/test_backward_compatibility.pyreturns 5/5 pass with the same byte-identity baselines on all 5 deterministic examples (web-app,microservices,ascii-web-api,mermaid-agentic-app,free-text-microservice). R9 before/after guardrail pair documents zero divergence. Themermaid-agentic-appbaseline was regenerated in T020 and confirmed byte-identical to the prior committed baseline underSOURCE_DATE_EPOCH=1700000000— the regeneration was a no-op verification, not a new artifact.scripts/extract-report-data.pyare ones already imported (shutil,subprocess,concurrent.futures,tempfile). Nopip install, no newrequirements.txtline, no new PyPI package.agentic-app/sample-report/stays excluded fromBASELINE_EXAMPLES: Per architect refinement R8 and task T019 (explicit negative task), the Feature 128 decision to exclude this example from the byte-deterministic baseline set stands. Feature 130 does not revisit that decision. The regeneration in T021 is a correctness smoke test (47 trees render), not a baseline addition.mmdcis unexpectedly present on PATH (plan Risk feat(005): implement STRIDE threat agents #6 mitigation). Previously this was observability-only (which mmdc || echo "expected absence"). The enforcement assertion ensures that if a future GitHub Actions runner image change ships withmmdcpreinstalled, the workflow breaks visibly instead of silently validating a happy path when we intended to validate the loud-failure path.attack-trees/." Projects using tachi for threat modeling without the Feature 112 attack-path output continue to work unchanged withoutmmdcinstalled.Review Checklist / Test Plan
git fetch origin && git checkout 130-prd-130-fixpytest tests/scripts/test_mmdc_preflight.py -v— expect 9 passedSOURCE_DATE_EPOCH=1700000000 pytest tests/scripts/test_backward_compatibility.py -v— expect 5 passedpytest tests/— expect 48/48 passexamples/mermaid-agentic-app/security-report.pdf.baselineand scroll to the Attack Path section. Confirm every attack path page shows a rendered Mermaid diagram image (not rawflowchart TDsource). Node labels and edge connections should be legible at page size per spec 112 SC-003.examples/agentic-app/sample-report/security-report.pdfand confirm all 17 Critical/High attack path pages render as images. (This example is intentionally excluded from the byte-deterministic baseline set per Feature 128; the visual check is the correctness gate.)mmdcpresent, run:env -i PATH="/usr/bin:/bin" /usr/bin/python3 scripts/extract-report-data.py \ --target-dir examples/mermaid-agentic-app/ \ --output /tmp/out.typ \ --template-dir templates/tachi/security-report/RuntimeErrorcontaining@mermaid-js/mermaid-cli,npm install -g @mermaid-js/mermaid-cli, andAttack path rendering. Confirm no PDF is produced at/tmp/out.typ.pdfor elsewhere..github/workflows/tachi-mmdc-preflight.ymlworkflow to pass on this PR. Confirm the diagnostic step logs "expected absence: mmdc not on PATH, preflight gate should fire" and the assertion step confirms all 3 canonical tokens appear in captured stderr.docs/architecture/02_ADRs/ADR-022-mmdc-hard-prerequisite.mdfor the decision rationale, the Future Work clause on helper extraction, and the cross-references to ADR-014 and ADR-021. Confirm the ADR accurately reflects the shipped implementation.grep -n 'else if mermaid-text' templates/tachi/security-report/attack-path.typ— expect zero results.grep -rn 'npm install -g @mermaid-js/mermaid-cli'— expect exactly 7 locations (extract-report-data.py, command file, install.sh, README, test file, CI workflow, ADR-022).🤖 Generated with Claude Code