Skip to content

Commit 7f047b7

Browse files
fix(154): PDF report — attack trees, MAESTRO headings, landscape whitespace (#155)
Three downstream rendering bugs surfaced in second-brain-mcp's generated security-report.pdf. Two share a root cause; one is an independent template bug. Root cause A — scripts/ missing from INSTALL_MANIFEST.md The install manifest never distributed scripts/extract-report-data.py, scripts/extract-infographic-data.py, or scripts/tachi_parsers.py to target projects. When /tachi.security-report ran in a downstream project, the report-assembler agent's `python3 scripts/extract-report-data.py` invocation failed silently and the agent fell through to a legacy LLM-based inline extraction path, producing report-data.typ with has-image: false on every attack-tree entry (skipping the Mermaid visuals) and empty layer-id / layer-name on MAESTRO findings-by-layer groups (collapsing headings to bare "—"). The PDF compiled cleanly, so failures were invisible to users. Fixes for root cause A: - INSTALL_MANIFEST.md: add scripts/extract-report-data.py, scripts/extract-infographic-data.py, scripts/tachi_parsers.py to both the human-readable Distributable Directories table and the machine-parseable <!-- BEGIN MANIFEST --> section. Add a new "Script Files" section documenting the distribution requirement and explicitly flagging the silent-failure mode so it cannot recur. - .claude/agents/tachi/report-assembler.md: add a mandatory Step 2b preflight check (`test -f scripts/extract-report-data.py && test -f scripts/tachi_parsers.py`) that hard-fails with an install.sh pointer when the scripts are missing. Renumber subsequent steps (old 2b to 2c, old 2c to 2d, old 2d to 2e). Add exit code 127 to the exit-code table. Rewrite the legacy-reference paragraph as a "Deprecated inline extraction path — DO NOT USE" warning that enumerates the observed failure modes (missing has-image, empty MAESTRO fields). - .claude/agents/tachi/threat-infographic.md: add a symmetric preflight check for scripts/extract-infographic-data.py and scripts/tachi_parsers.py. Root cause B — landscape infographic whitespace Feature 128 introduced a fixed-height 7.5in block in full-bleed.typ's infographic-page() to accommodate the portrait executive-architecture infographic (912x1168). Landscape 1376x768 infographics (risk-funnel, baseball-card, system-architecture, maestro-stack, maestro-heatmap) scale to ~3.6in tall and were vertically centered inside the 7.5in block by `fit: "contain"`, producing ~1.9in of wasted whitespace above and below the image with the border stretched around the dead space. Fix for root cause B: - templates/tachi/security-report/full-bleed.typ: add is-portrait parameter (default false). For landscape, use a block with width: 100% and no height constraint so the block hugs the image's natural aspect height. For portrait, keep the 7.5in height cap to prevent overflow and center the block horizontally via align(). Both paths use inset: 0pt so the border wraps tightly around the image. - templates/tachi/security-report/main.typ: pass is-portrait: true only to the executive-architecture infographic-page call; all other landscape calls keep the default. Verification: - examples/agentic-app/sample-report/security-report.pdf regenerated (SOURCE_DATE_EPOCH=1700000000) — executive-architecture portrait page and five landscape infographic pages all render with tight borders and no dead whitespace; MAESTRO Layer Analysis headings show full canonical names (L1 Foundation Model, L5 Evaluation and Observability, etc.); 17/17 attack tree Mermaid PNGs rendered inline on their respective Attack Path Analysis pages. - All 47 tests in tests/scripts/ pass. - All 5 backward-compat baselines remain byte-identical under the preserved SOURCE_DATE_EPOCH=1700000000 contract (web-app, microservices, ascii-web-api, mermaid-agentic-app, free-text-microservice): none of these examples use infographic-page() so the template change has no rendering side effect, and the unchanged main.typ call sites produce identical compiled output. Closes #154 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 32e46a6 commit 7f047b7

File tree

6 files changed

+120
-18
lines changed

6 files changed

+120
-18
lines changed

.claude/agents/tachi/report-assembler.md

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,40 @@ Check for a user-provided `report-config.typ` in the target directory:
117117
- Ensure the default `templates/tachi/security-report/report-config.typ` exists (it ships with the templates)
118118
- Log: `"Using default report configuration"`
119119
120-
### 2b. Invoke Extraction Script
120+
### 2b. Preflight: Verify Script Exists
121+
122+
**MANDATORY HARD-FAIL CHECK** — before invoking the extraction script, verify it exists on disk:
123+
124+
```bash
125+
test -f scripts/extract-report-data.py && test -f scripts/tachi_parsers.py
126+
```
127+
128+
If either file is missing, abort with this exact error and do NOT fall through to any alternate extraction path:
129+
130+
```
131+
EXTRACTION SCRIPT MISSING
132+
133+
Required files not found:
134+
- scripts/extract-report-data.py
135+
- scripts/tachi_parsers.py
136+
137+
These files are distributed by tachi's scripts/install.sh and must exist
138+
in the project's scripts/ directory. If this project was installed before
139+
PR #154, re-run the installer:
140+
141+
~/Projects/tachi/scripts/install.sh
142+
143+
Or manually copy the missing files from the tachi source tree.
144+
145+
DO NOT generate report-data.typ inline — the deterministic Python
146+
extraction is the only supported path. LLM-based inline extraction
147+
silently produces field-incomplete output (missing attack-tree images,
148+
empty MAESTRO layer headings, missing MAESTRO data).
149+
```
150+
151+
Return failure to the command. Do NOT proceed to Step 2c, Step 2d, or Step 3 under any circumstances when the scripts are missing.
152+
153+
### 2c. Invoke Extraction Script
121154

122155
Run the deterministic extraction script:
123156

@@ -131,23 +164,24 @@ python3 scripts/extract-report-data.py \
131164

132165
Include `--title` only if the command provided a title override.
133166

134-
### 2c. Handle Exit Codes
167+
### 2d. Handle Exit Codes
135168

136169
| Exit Code | Meaning | Agent Action |
137170
|-----------|---------|-------------|
138171
| 0 | Success | Proceed to Step 3 (Compilation) |
139172
| 1 | Missing required artifact | Display stderr message and abort: `"Error: {message}"` |
140173
| 2 | Validation failure | Display stderr details and abort: `"Validation error: {details}"` |
174+
| 127 | `python3` or script not found | Display: `"Error: extraction script invocation failed. Verify scripts/extract-report-data.py is executable and python3 is on PATH. Re-run scripts/install.sh if the script is missing."` and abort |
141175

142-
If the script exits with code 1 or 2, do NOT proceed to compilation. Display the error and return failure to the command.
176+
If the script exits non-zero for any reason, do NOT proceed to compilation. Do NOT fall back to inline LLM extraction. Display the error and return failure to the command.
143177

144-
### 2d. Report Results
178+
### 2e. Report Results
145179

146180
Display: `"report-data.typ generated — proceeding to compilation"`
147181

148182
---
149183

150-
**Legacy reference**: The previous Steps 2-3 performed LLM-based markdown parsing and Typst generation inline. The Python script (`scripts/extract-report-data.py`) replaces this with deterministic regex-based extraction. The Typst variable contract is identical -- all variable names, types, and structure match the templates. For the full variable specification, see `specs/067-deterministic-report-data/data-model.md`.
184+
**Deprecated inline extraction path — DO NOT USE**: Versions of this agent before PR #154 documented an LLM-based fallback for Steps 2-3 that parsed artifacts inline and generated `report-data.typ` by prompting the model. That path is deprecated and unsupported: it silently omits `has-image` on attack-tree entries (producing attack-path pages with no Mermaid visuals), emits empty `layer-id`/`layer-name` fields in MAESTRO findings-by-layer groups (producing headings that render as bare em-dashes), and skips other derived fields. The deterministic Python script (`scripts/extract-report-data.py`) is the only supported extraction path. For the full variable specification, see `specs/067-deterministic-report-data/data-model.md`.
151185

152186
---
153187

.claude/agents/tachi/threat-infographic.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,28 @@ Data extraction is performed by a deterministic Python script that replaces the
138138

139139
### Script Invocation
140140

141+
**Preflight (MANDATORY)**: Verify the extraction script and its parser helpers exist before invoking:
142+
143+
```bash
144+
test -f scripts/extract-infographic-data.py && test -f scripts/tachi_parsers.py
145+
```
146+
147+
If either file is missing, abort with:
148+
149+
```
150+
EXTRACTION SCRIPT MISSING
151+
152+
Required files not found:
153+
- scripts/extract-infographic-data.py
154+
- scripts/tachi_parsers.py
155+
156+
Re-run the tachi installer to distribute these files:
157+
158+
~/Projects/tachi/scripts/install.sh
159+
160+
DO NOT attempt LLM-based inline extraction as a fallback.
161+
```
162+
141163
Run the extraction script before generating the specification:
142164

143165
```bash

INSTALL_MANIFEST.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Canonical list of files and directories that must be copied when installing tach
1212
| `templates/tachi/output-schemas/` | Canonical output format templates | orchestrator, risk-scorer, control-analyzer, threat-report |
1313
| `templates/tachi/infographics/` | Infographic design templates | threat-infographic |
1414
| `templates/tachi/security-report/` | Typst PDF report templates | report-assembler |
15+
| `scripts/` (3 Python files) | Deterministic extraction scripts | report-assembler, threat-infographic |
1516
| `adapters/claude-code/agents/references/` | SARIF generation and validation guides | risk-scorer, control-analyzer |
1617
| `brand/` | Logo assets for branded PDF reports | report-assembler |
1718
| `docs/guides/DEVELOPER_GUIDE_TACHI.md` | Full walkthrough with worked examples | User reference |
@@ -53,6 +54,22 @@ Copy the entire `.claude/agents/tachi/` directory. Current agents:
5354
| `threat-infographic.md` | Visual infographic generator |
5455
| `report-assembler.md` | PDF report assembler |
5556

57+
## Script Files
58+
59+
Copy these 3 Python files from `scripts/` to the target project's `scripts/`:
60+
61+
| File | Purpose | Invoked By |
62+
|------|---------|------------|
63+
| `extract-report-data.py` | Parses tachi artifacts and generates `report-data.typ` for Typst compilation; also renders attack-tree Mermaid blocks to PNG via mmdc | report-assembler (via `/tachi.security-report`) |
64+
| `extract-infographic-data.py` | Parses tachi artifacts and generates infographic specification JSON | threat-infographic (via `/tachi.infographic`) |
65+
| `tachi_parsers.py` | Shared parser helpers imported by both extraction scripts | extract-report-data.py, extract-infographic-data.py |
66+
67+
Scripts use stdlib-only imports — no pip dependencies required in the target project.
68+
69+
**Critical**: If these scripts are missing, the report-assembler and threat-infographic agents will silently fall through to LLM-based inline extraction, producing technically-compiling but field-incomplete outputs (missing attack-tree images, empty MAESTRO layer headings, missing infographic data). Always include these files when distributing tachi.
70+
71+
Other files in `scripts/` (`check.sh`, `install.sh`, `generate-adapter-version.sh`, `polish-release-notes.sh`, `sync-upstream.sh`) are tachi-internal and are NOT distributed to target projects.
72+
5673
## Schema Files
5774

5875
Copy the entire `schemas/` directory. Current schemas:
@@ -80,6 +97,9 @@ The install script parses this section automatically. One path per line — dire
8097
.claude/commands/tachi.architecture.md
8198
schemas/
8299
templates/tachi/
100+
scripts/extract-report-data.py
101+
scripts/extract-infographic-data.py
102+
scripts/tachi_parsers.py
83103
adapters/claude-code/agents/references/
84104
brand/
85105
docs/guides/DEVELOPER_GUIDE_TACHI.md
@@ -95,6 +115,7 @@ When adding a new feature, check whether it requires updates to:
95115
- [ ] A new output template in `templates/tachi/output-schemas/`
96116
- [ ] A new infographic template in `templates/tachi/infographics/`
97117
- [ ] New report page templates in `templates/tachi/security-report/`
118+
- [ ] A new distributable Python script in `scripts/` -- add to script table above
98119
- [ ] New reference docs in `adapters/claude-code/agents/references/`
99120
- [ ] Update install instructions in `README.md` and `docs/guides/DEVELOPER_GUIDE_TACHI.md`
100121
- [ ] Update the machine-parseable manifest section (`<!-- BEGIN MANIFEST -->` / `<!-- END MANIFEST -->`)
-3.17 KB
Binary file not shown.

templates/tachi/security-report/full-bleed.typ

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,23 @@
3636
// section-name (content|none) — heading text for the page and TOC.
3737
// classification (string|none) — classification marking for header bar.
3838
// description (content|none) — explanatory text rendered below the image.
39+
// is-portrait (bool) — true for portrait-aspect images (e.g.,
40+
// executive-architecture 912×1168); false
41+
// (default) for landscape 16:9 infographics
42+
// (1376×768) like risk-funnel, baseball-card,
43+
// maestro-stack, maestro-heatmap, etc.
44+
// Landscape images are sized by content
45+
// width so the border hugs the image and
46+
// no dead whitespace surrounds it. Portrait
47+
// images are capped at 7.5in tall to avoid
48+
// page overflow.
3949

4050
#let infographic-page(
4151
image-path,
4252
section-name: none,
4353
classification: none,
4454
description: none,
55+
is-portrait: false,
4556
) = {
4657
page(
4758
width: page-width,
@@ -62,19 +73,32 @@
6273
heading(level: 1)[#section-name]
6374
}
6475

65-
// Image with rounded corners and subtle border, proportionally scaled.
66-
// Height is constrained so portrait-aspect images (e.g., executive-architecture)
67-
// don't overflow onto subsequent pages. `fit: "contain"` preserves aspect ratio
68-
// within the 100% x 7.5in bounding box.
69-
#block(
70-
width: 100%,
71-
height: 7.5in,
72-
radius: 4pt,
73-
clip: true,
74-
stroke: 0.5pt + color-rule,
75-
)[
76-
#image(image-path, width: 100%, height: 100%, fit: "contain")
77-
]
76+
// Image block: orientation-aware sizing so the border hugs the image
77+
// without leaving dead whitespace above or below.
78+
// - Landscape: block fills content width; height derives from image aspect
79+
// - Portrait: block capped at 7.5in tall; width derives from image aspect
80+
#if is-portrait {
81+
align(center,
82+
block(
83+
radius: 4pt,
84+
clip: true,
85+
stroke: 0.5pt + color-rule,
86+
inset: 0pt,
87+
)[
88+
#image(image-path, height: 7.5in)
89+
],
90+
)
91+
} else {
92+
block(
93+
width: 100%,
94+
radius: 4pt,
95+
clip: true,
96+
stroke: 0.5pt + color-rule,
97+
inset: 0pt,
98+
)[
99+
#image(image-path, width: 100%)
100+
]
101+
}
78102

79103
// Explanatory text below the image.
80104
#if description != none {

templates/tachi/security-report/main.typ

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@
204204
executive-architecture-image-path,
205205
section-name: "Executive Threat Architecture",
206206
classification: classification,
207+
is-portrait: true,
207208
description: [
208209
Layered system architecture with critical and high severity threats annotated as narrative callouts. This visualization highlights where the most exposed components sit in the system and what kind of attack each layer is most vulnerable to.
209210
],

0 commit comments

Comments
 (0)