Skip to content

Commit f2ad9be

Browse files
davidmatousekclaude
andcommitted
feat(154): deterministic Gemini prompt scaffold for infographic quality stability
Root cause (5 Whys): infographic image quality degraded because the agent rewrote the template's fixed visual directives (dark navy background, severity colors, typography) when constructing the Gemini prompt. The architecture split data extraction (deterministic) from prompt construction (fully LLM-driven) with no guardrails on the styling scaffold. Option D implementation: the Python extraction script now reads each template file, extracts the Gemini prompt section, and splits it at the "DATA CONTENT" marker into a locked preamble (opening aesthetic + IMPORTANT note + STYLING DIRECTIVES) and postamble (FOOTER + closing). These go into the JSON output as `prompt_scaffold.preamble` and `prompt_scaffold.postamble`. The agent instructions now require VERBATIM use of the scaffold — the agent fills only the DATA CONTENT sections from JSON data. This locks the visual design (dark navy, 3D effects, severity colors) while preserving LLM flexibility for data narrative descriptions. Changes: - scripts/extract-infographic-data.py: add extract_prompt_scaffold() that reads template files and splits at "DATA CONTENT (render this" marker, include prompt_scaffold in JSON output via build_json_output() - .claude/agents/tachi/threat-infographic.md: add "Gemini Prompt Construction — Scaffold" section with MANDATORY verbatim-use protocol - .claude/skills/tachi-infographics/references/gemini-prompt-construction.md: replace "Design Template Loading" with "Prompt Scaffold (Option D)" protocol, document preamble/postamble usage, add fallback for templates without scaffolds (executive-architecture) - Golden baselines regenerated for all 5 templates - 47/47 tests pass Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3cd5d27 commit f2ad9be

File tree

8 files changed

+202
-4
lines changed

8 files changed

+202
-4
lines changed

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,10 +215,16 @@ The script outputs a JSON file with this top-level structure:
215215
"top_findings": [
216216
{ "id": "S-001", "component": "API Gateway", "threat": "...", "risk_level": "Critical", "score": 9.2 }
217217
],
218-
"template_data": { }
218+
"template_data": { },
219+
"prompt_scaffold": {
220+
"preamble": "Create a premium, professional... [locked styling scaffold]",
221+
"postamble": "FOOTER: ... [locked closing statement]"
222+
}
219223
}
220224
```
221225

226+
When `prompt_scaffold` is present, it contains the **locked visual design directives** extracted from the infographic template file. See "Gemini Prompt Construction — Scaffold" below for how to use it.
227+
222228
The complete JSON schema is defined in `specs/071-deterministic-infographic-extraction/data-model.md`. The `template_data` object varies by template -- see the data model for `baseball-card`, `system-architecture`, and `risk-funnel` schemas.
223229

224230
---
@@ -235,6 +241,20 @@ The output `threat-{template-name}-spec.md` contains YAML frontmatter and 6 requ
235241

236242
---
237243

244+
## Gemini Prompt Construction — Scaffold
245+
246+
**MANDATORY**: Read `.claude/skills/tachi-infographics/references/gemini-prompt-construction.md` Section "Design Template Loading — Prompt Scaffold (Option D)" for the full protocol.
247+
248+
When the JSON output contains a `prompt_scaffold` object, you **MUST** use it:
249+
250+
1. **Copy `prompt_scaffold.preamble` VERBATIM** — do NOT rewrite any part of it (background color, styling directives, aesthetic target are LOCKED)
251+
2. **Write DATA CONTENT sections** from JSON data (severity counts, findings, heat map, scores) — this is where you have creative flexibility
252+
3. **Copy `prompt_scaffold.postamble` VERBATIM** — do NOT rewrite the footer or closing statement
253+
254+
This ensures every run uses the same dark-navy (or template-appropriate) background, severity colors, and layout directives. Without the scaffold, previous runs produced white-background flat images instead of the premium dark-themed 3D visuals.
255+
256+
---
257+
238258
## Executive-Architecture Gemini Prompt Construction
239259

240260
When generating the `threat-executive-architecture.jpg` image via Gemini API, the prompt MUST instruct Gemini to:

.claude/skills/tachi-infographics/references/gemini-prompt-construction.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,33 @@ Rules and patterns for constructing Gemini API image generation prompts from inf
44

55
---
66

7-
## Design Template Loading
7+
## Design Template Loading — Prompt Scaffold (Option D)
88

9-
After generating the specification (`threat-{template-name}-spec.md`), construct a Gemini image generation prompt using the active design template.
9+
The extraction script (`scripts/extract-infographic-data.py`) outputs a `prompt_scaffold` object in the JSON with two fields:
10+
- **`preamble`**: the opening aesthetic instruction, IMPORTANT note, and STYLING DIRECTIVES block — everything up to and including the "DATA CONTENT (render this as visible text):" header.
11+
- **`postamble`**: the FOOTER specification and closing aesthetic instruction.
1012

11-
### Template Location
13+
### MANDATORY: Use Scaffold Verbatim
14+
15+
When `prompt_scaffold` is present in the JSON output, you **MUST** construct the Gemini prompt by:
16+
17+
1. **Copy `prompt_scaffold.preamble` VERBATIM** as the start of the prompt. Do NOT rewrite, paraphrase, or modify ANY part of it — the background color, styling directives, layout instructions, and aesthetic target are locked.
18+
2. **Write the DATA CONTENT sections** using data from the JSON (severity counts, findings, heat map grid, etc.). This is the ONLY section where you have creative control.
19+
3. **Copy `prompt_scaffold.postamble` VERBATIM** as the end of the prompt. Do NOT rewrite the footer text or closing instructions.
20+
21+
```
22+
[preamble — VERBATIM from JSON, includes opening + IMPORTANT + STYLING DIRECTIVES + "DATA CONTENT" header]
23+
24+
[Your DATA CONTENT sections — written from JSON data, with specific counts, scores, finding descriptions]
25+
26+
[postamble — VERBATIM from JSON, includes FOOTER + closing aesthetic instruction]
27+
```
28+
29+
**Why this matters**: The scaffold locks the visual design (dark navy background, severity colors, typography, layout). Previous runs where the agent rewrote the scaffold produced white-background flat images instead of the premium dark-themed 3D visuals the templates specify.
30+
31+
### Fallback (no scaffold)
32+
33+
If `prompt_scaffold` is NOT present in the JSON (e.g., executive-architecture template, or older script version):
1234

1335
Load `templates/tachi/infographics/infographic-{name}.md` and use its **Gemini Prompt Template** section. Replace all `{placeholders}` with actual data from the infographic spec.
1436

scripts/extract-infographic-data.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,125 @@ def _canonical_severity(finding):
7878
return ""
7979

8080

81+
# =============================================================================
82+
# Gemini Prompt Scaffold Extraction
83+
# =============================================================================
84+
85+
# Templates that have a Gemini prompt section with the standard
86+
# PREAMBLE → DATA CONTENT → POSTAMBLE structure.
87+
_SCAFFOLD_TEMPLATES = frozenset({
88+
"baseball-card", "risk-funnel", "system-architecture",
89+
"maestro-stack", "maestro-heatmap",
90+
})
91+
92+
_TEMPLATE_FILES = {
93+
"baseball-card": "infographic-baseball-card.md",
94+
"risk-funnel": "infographic-risk-funnel.md",
95+
"system-architecture": "infographic-system-architecture.md",
96+
"maestro-stack": "infographic-maestro-stack.md",
97+
"maestro-heatmap": "infographic-maestro-heatmap.md",
98+
}
99+
100+
101+
def extract_prompt_scaffold(template_name: str, repo_root: Path = None) -> dict:
102+
"""Extract the fixed Gemini prompt scaffold from an infographic template.
103+
104+
Reads the template file, locates the Gemini prompt section (between
105+
triple-backtick fences), and splits it at the "DATA CONTENT" marker.
106+
107+
Returns:
108+
Dict with:
109+
- preamble: everything from prompt start through "DATA CONTENT (render
110+
this as visible text):" — includes opening aesthetic instruction,
111+
IMPORTANT note, and STYLING DIRECTIVES block.
112+
- postamble: the FOOTER line through the closing aesthetic instruction.
113+
- found: True if scaffold was successfully extracted.
114+
115+
If the template file or prompt section is not found, returns
116+
found=False with empty strings (graceful degradation — agent falls
117+
back to its own prompt construction).
118+
"""
119+
result = {"preamble": "", "postamble": "", "found": False}
120+
121+
if template_name not in _SCAFFOLD_TEMPLATES:
122+
return result
123+
124+
if repo_root is None:
125+
repo_root = Path(__file__).resolve().parent.parent
126+
127+
template_path = repo_root / "templates" / "tachi" / "infographics" / _TEMPLATE_FILES[template_name]
128+
if not template_path.exists():
129+
return result
130+
131+
content = template_path.read_text(encoding="utf-8")
132+
133+
# Extract the Gemini prompt block (first triple-backtick fence after
134+
# a heading containing "Gemini" and "Prompt")
135+
prompt_text = None
136+
lines = content.split("\n")
137+
in_prompt_section = False
138+
in_fence = False
139+
fence_lines = []
140+
141+
for line in lines:
142+
stripped = line.strip()
143+
if re.match(r"^#{1,4}\s+.*[Gg]emini.*[Pp]rompt", stripped):
144+
in_prompt_section = True
145+
continue
146+
if in_prompt_section and not in_fence and stripped.startswith("```"):
147+
in_fence = True
148+
continue
149+
if in_fence and stripped.startswith("```"):
150+
prompt_text = "\n".join(fence_lines)
151+
break
152+
if in_fence:
153+
fence_lines.append(line)
154+
155+
if not prompt_text:
156+
return result
157+
158+
# Split at the standalone "DATA CONTENT" section marker.
159+
# The phrase "DATA CONTENT" also appears inside the IMPORTANT note
160+
# ("...specified in the DATA CONTENT sections."), so we match the
161+
# full section header form to avoid a false-positive split.
162+
data_marker = "DATA CONTENT (render this"
163+
marker_idx = prompt_text.find(data_marker)
164+
if marker_idx == -1:
165+
# Fallback: try bare marker at start of line
166+
for m in re.finditer(r"^DATA CONTENT", prompt_text, re.MULTILINE):
167+
# Skip if this is the IMPORTANT note reference
168+
line_end = prompt_text.find("\n", m.start())
169+
line = prompt_text[m.start():line_end if line_end != -1 else len(prompt_text)]
170+
if "sections." not in line:
171+
marker_idx = m.start()
172+
break
173+
if marker_idx == -1:
174+
return result
175+
176+
# Find the full marker line end
177+
marker_line_end = prompt_text.find("\n", marker_idx)
178+
if marker_line_end == -1:
179+
marker_line_end = len(prompt_text)
180+
181+
preamble = prompt_text[:marker_line_end + 1].rstrip() + "\n"
182+
183+
# Postamble: from "FOOTER" to end of prompt
184+
footer_marker = "\nFOOTER"
185+
footer_idx = prompt_text.find(footer_marker)
186+
if footer_idx == -1:
187+
# Try without leading newline
188+
footer_idx = prompt_text.find("FOOTER")
189+
if footer_idx != -1:
190+
postamble = prompt_text[footer_idx:].strip()
191+
else:
192+
postamble = ""
193+
194+
result["preamble"] = preamble
195+
result["postamble"] = postamble
196+
result["found"] = True
197+
return result
198+
199+
81200
# =============================================================================
82201
# T009: Largest Remainder Method
83202
# =============================================================================
@@ -1395,6 +1514,10 @@ def build_json_output(data, template):
13951514
if "delta" in data:
13961515
output["delta"] = data["delta"]
13971516

1517+
# Add prompt scaffold when extracted from template
1518+
if "prompt_scaffold" in data:
1519+
output["prompt_scaffold"] = data["prompt_scaffold"]
1520+
13981521
# Add template to metadata
13991522
output["metadata"]["template"] = template
14001523

@@ -1611,6 +1734,14 @@ def main():
16111734
"delta_counts": compute_delta_counts(findings, resolved),
16121735
}
16131736

1737+
# Extract prompt scaffold from template file (Option D: locked styling,
1738+
# flexible data narrative). The scaffold contains the opening aesthetic
1739+
# instruction, STYLING DIRECTIVES, and closing statement — all the fixed
1740+
# visual directives that must not be rewritten by the agent.
1741+
scaffold = extract_prompt_scaffold(args.template)
1742+
if scaffold["found"]:
1743+
print(f"Prompt scaffold extracted from template ({args.template})", file=sys.stderr)
1744+
16141745
# Assemble data dict
16151746
data = {
16161747
"metadata": metadata,
@@ -1620,6 +1751,11 @@ def main():
16201751
"findings_ids": findings_ids,
16211752
"template_data": template_data,
16221753
}
1754+
if scaffold["found"]:
1755+
data["prompt_scaffold"] = {
1756+
"preamble": scaffold["preamble"],
1757+
"postamble": scaffold["postamble"],
1758+
}
16231759
if delta_data:
16241760
data["delta"] = delta_data
16251761

tests/scripts/fixtures/golden/baseball-card.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@
6969
"tier": 1,
7070
"total_findings": 34
7171
},
72+
"prompt_scaffold": {
73+
"postamble": "FOOTER: \"Generated by Tachi Threat Modeling Framework \u2014 STRIDE + AI Threat Analysis\" in small light gray text, centered.\n\nThe overall impression should be a polished professional report \u2014 confident, clear, and visually sophisticated. No hex codes, color values, or technical specifications should appear as visible text. Render the dashboard as a flat, full-bleed graphic filling the entire 16:9 frame. No perspective, no 3D, no boardroom, no table, no environmental context.",
74+
"preamble": "Create a premium, professional security risk dashboard with a polished, modern dark-theme aesthetic. This should look like a professionally designed Figma dashboard \u2014 not a data table or spreadsheet. The overall feel should be confident, sophisticated, and visually impressive \u2014 a formal security report artifact, not a presentation slide or boardroom scene. Render ONLY the dashboard itself as a flat document \u2014 no perspective, no 3D effects, no room or table context, no environmental background. The image should be the report, not a photo of the report.\n\nIMPORTANT: The styling directives below are for your interpretation only. Do NOT render any hex color codes, pixel values, font sizes, or technical CSS specifications as visible text in the image. Only render the data labels, numbers, and natural-language text specified in the DATA CONTENT sections.\n\nSTYLING DIRECTIVES (interpret these, do not display them):\n- Background: dark navy\n- Severity color mapping: Critical = red, High = orange, Medium = amber/yellow, Low = blue\n- All text on dark background: white or light gray\n- Cards and panels: rounded corners, subtle drop shadows, generous whitespace\n- Layout: 16:9 landscape\n- Empty heat map cells: subtle dark gray\n\nDATA CONTENT (render this as visible text):\n"
75+
},
7276
"severity_distribution": [
7377
{
7478
"color": "#DC2626",

tests/scripts/fixtures/golden/maestro-heatmap.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@
6969
"tier": 1,
7070
"total_findings": 34
7171
},
72+
"prompt_scaffold": {
73+
"postamble": "FOOTER: \"Generated by Tachi Threat Modeling Framework \u2014 CSA MAESTRO Layer Analysis\" in small light gray text, centered.\n\nThe overall impression should be a polished professional report \u2014 confident, clear, and visually sophisticated. No hex codes, color values, or technical specifications should appear as visible text. Render the dashboard as a flat, full-bleed graphic filling the entire 16:9 frame. No perspective, no 3D, no boardroom, no table, no environmental context.",
74+
"preamble": "Create a premium, professional MAESTRO component-layer heatmap dashboard with a polished, modern dark-theme aesthetic. This should look like a professionally designed Figma dashboard \u2014 not a data table or spreadsheet. The overall feel should be confident, sophisticated, and visually impressive \u2014 a formal security report artifact, not a presentation slide or boardroom scene. Render ONLY the dashboard itself as a flat document \u2014 no perspective, no 3D effects, no room or table context, no environmental background. The image should be the report, not a photo of the report.\n\nIMPORTANT: The styling directives below are for your interpretation only. Do NOT render any hex color codes, pixel values, font sizes, or technical CSS specifications as visible text in the image. Only render the data labels, numbers, and natural-language text specified in the DATA CONTENT sections.\n\nSTYLING DIRECTIVES (interpret these, do not display them):\n- Background: dark navy\n- Severity color mapping: Critical = red, High = orange, Medium = amber/yellow, Low = blue\n- Empty grid cells: subtle dark gray rounded rectangles\n- All text on dark background: white or light gray\n- Grid cells: rounded rectangles with generous padding\n- Legend panel: right side, visually distinct section with color swatches\n- Layout: 16:9 landscape\n\nDATA CONTENT (render this as visible text):\n"
75+
},
7276
"severity_distribution": [
7377
{
7478
"color": "#DC2626",

tests/scripts/fixtures/golden/maestro-stack.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@
6969
"tier": 1,
7070
"total_findings": 34
7171
},
72+
"prompt_scaffold": {
73+
"postamble": "FOOTER: \"Generated by Tachi Threat Modeling Framework \u2014 CSA MAESTRO Layer Analysis\" in small light gray text, centered.\n\nThe overall impression should be a polished professional report \u2014 confident, clear, and visually sophisticated. No hex codes, color values, or technical specifications should appear as visible text. Render the dashboard as a flat, full-bleed graphic filling the entire 16:9 frame. No perspective, no 3D, no boardroom, no table, no environmental context.",
74+
"preamble": "Create a premium, professional security risk dashboard with a polished, modern dark-theme aesthetic. This should look like a professionally designed Figma dashboard \u2014 not a data table or spreadsheet. The overall feel should be confident, sophisticated, and visually impressive \u2014 a formal security report artifact, not a presentation slide or boardroom scene. Render ONLY the dashboard itself as a flat document \u2014 no perspective, no 3D effects, no room or table context, no environmental background. The image should be the report, not a photo of the report.\n\nIMPORTANT: The styling directives below are for your interpretation only. Do NOT render any hex color codes, pixel values, font sizes, or technical CSS specifications as visible text in the image. Only render the data labels, numbers, and natural-language text specified in the DATA CONTENT sections.\n\nSTYLING DIRECTIVES (interpret these, do not display them):\n- Background: dark navy\n- Severity color mapping: Critical = red, High = orange, Medium = amber/yellow, Low = blue\n- All text on dark background: white or light gray\n- Cards and bands: rounded corners, subtle drop shadows, generous whitespace\n- Layout: 16:9 landscape, 3-zone (top header, main body split into stack + sidebar, footer)\n- Layer bands: horizontal bars stacked vertically, L7 at top through L1 at bottom\n- Most-exposed layer band: brighter background, wider left border accent\n- Empty layer bands: muted, darker background, grayed text\n\nDATA CONTENT (render this as visible text):\n"
75+
},
7276
"severity_distribution": [
7377
{
7478
"color": "#DC2626",

tests/scripts/fixtures/golden/risk-funnel.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@
6969
"tier": 1,
7070
"total_findings": 34
7171
},
72+
"prompt_scaffold": {
73+
"postamble": "FOOTER (bottom): \"Generated by Tachi Threat Modeling Framework \u2014 Risk Reduction Funnel\" in small light gray text, centered.\n\nThe overall impression should be a polished, premium risk reduction narrative \u2014 confident, clear, and visually sophisticated. Professional business language throughout, no technical jargon or color codes. Render as a flat, full-bleed graphic filling the entire 16:9 frame.",
74+
"preamble": "Create a premium, photorealistic 3D risk reduction funnel with glass-like translucent material, soft ambient lighting, and executive boardroom quality. This should look like a professionally designed data visualization for a CISO's board presentation \u2014 sophisticated, confident, and visually impressive. Render ONLY the infographic itself as a flat document \u2014 no perspective, no room context, no environmental background.\n\nIMPORTANT: The styling directives below are for your interpretation only. Do NOT render any hex color codes, pixel values, font sizes, percentages-of-height, or technical CSS specifications as visible text in the image. Only render the data labels, numbers, and natural-language text specified in the DATA CONTENT sections.\n\nSTYLING DIRECTIVES (interpret these, do not display them):\n- Background: dark navy\n- Severity color mapping: Critical = red, High = orange, Medium = amber/yellow, Low = blue\n- Ghost tier style: translucent gray with dashed border\n- All text on dark background: white or light gray\n- Panels: rounded corners, subtle drop shadows, generous whitespace\n- Layout: 16:9 landscape, premium executive aesthetic\n- Funnel tiers: translucent 3D trapezoids with glass-like material, soft ambient lighting, gradient connectors between tiers\n- CONFIDENTIAL badge: red pill with white text\n\nDATA CONTENT (render this as visible text):\n"
75+
},
7276
"severity_distribution": [
7377
{
7478
"color": "#DC2626",

0 commit comments

Comments
 (0)