Commit d35a667
fix(130): enforce mmdc as hard prerequisite with loud preflight/mid-render aborts (#148)
* docs(130): scaffold feature workspace with PRD, spec, plan, ADR-022
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>
* fix(130): add mmdc preflight gate with defense-in-depth
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>
* fix(130): abort on mid-render failure with per-finding error list
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>
* docs(130): declare mmdc as hard prerequisite across README, install.sh, 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>
* chore(130): CI workflow, docs, and Wave 6 verification
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>
* docs(130): PR description, docstring cleanup, Wave 7 polish
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>
* security(130): run security scan [2c97e80]
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>
* chore(130): mark T030/T031 complete, finalize NEXT-SESSION notes
T030: full pytest suite 48/48 green
T031: conventional commits, stdlib-only, branch hygiene verified
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>1 parent 7240695 commit d35a667
File tree
28 files changed
+3526
-60
lines changed- .claude/commands
- .github/workflows
- .security
- reports
- docs
- architecture
- 00_Tech_Stack
- 01_system_design
- 02_ADRs
- product
- 02_PRD
- _backlog
- scripts
- specs
- 112-attack-path-pages
- 130-prd-130-fix
- checklists
- templates/tachi/security-report
- tests/scripts
28 files changed
+3526
-60
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
56 | | - | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
57 | 68 | | |
58 | 69 | | |
59 | 70 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
101 | 111 | | |
102 | 112 | | |
103 | 113 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
30 | 59 | | |
31 | 60 | | |
32 | 61 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
269 | 269 | | |
270 | 270 | | |
271 | 271 | | |
272 | | - | |
| 272 | + | |
273 | 273 | | |
274 | 274 | | |
275 | 275 | | |
276 | 276 | | |
277 | 277 | | |
278 | 278 | | |
279 | | - | |
| 279 | + | |
280 | 280 | | |
281 | 281 | | |
282 | 282 | | |
| |||
0 commit comments