Skip to content

Commit 3cd5d27

Browse files
davidmatousekclaude
andcommitted
fix(154): infographic quality — extract risk metrics, update Gemini model config
Two fixes for degraded infographic quality observed in downstream projects. 1. Extract risk summary metrics from compensating-controls.md Section 1 tachi_parsers.py: parse_compensating_controls_md() now extracts risk_reduction (e.g., 22.9), inherent_score (270.3), residual_score (208.5), and control_coverage_pct (26.0) from the Executive Summary "Risk Reduction" and "Coverage" lines. extract-infographic-data.py: pass these four fields through to template_data for both baseball-card and risk-funnel templates. The infographic agent now has the quantitative data needed to construct rich, specific Gemini prompts instead of the vague "0% reduction" prompts that produced flat, schematic images. 2. Update Gemini model config with fallback chain gemini-prompt-construction.md: change default_model from gemini-3-pro-image-preview (preview, may not be accessible) to gemini-2.5-flash-image (GA stable). Add a fallback_chain that tries gemini-3-pro-image-preview first (highest quality), then gemini-3.1-flash-image-preview, then gemini-2.5-flash-image. Document model aliases (nano-banana family) and clarify that -image suffix models are distinct from base text models. Golden baselines regenerated for baseball-card and risk-funnel templates. 47/47 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 30f9ad9 commit 3cd5d27

File tree

5 files changed

+75
-4
lines changed

5 files changed

+75
-4
lines changed

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,28 @@ Apply these labels in the Gemini prompt based on `metadata.data_source_type`:
121121

122122
```yaml
123123
gemini_config:
124-
default_model: "gemini-3-pro-image-preview"
124+
default_model: "gemini-2.5-flash-image"
125+
fallback_chain:
126+
- "gemini-3-pro-image-preview" # Highest quality (preview — may not be available on all API keys)
127+
- "gemini-3.1-flash-image-preview" # Fast, production-scale (preview)
128+
- "gemini-2.5-flash-image" # Stable GA — broadest availability, reliable fallback
125129
resolution: "2K"
126130
```
127131
128-
- **default_model**: The primary Gemini model for image generation. Configurable -- do not hardcode.
132+
- **default_model**: The GA-stable Gemini model for image generation. Use this when preview models are unavailable. Configurable -- do not hardcode.
133+
- **fallback_chain**: Try models in order. Preview models (`-preview` suffix) produce higher quality output but may not be accessible on all API keys or regions. The agent should attempt the first available model and fall back through the chain on 404 or model-not-found errors.
129134
- **resolution**: Target output resolution. "2K" produces images at approximately 1920x1080 for 16:9 aspect ratio.
130135

136+
**Model aliases** (for reference — these are NOT model IDs, just human-friendly names):
137+
138+
| Alias | Model ID | Status | Best For |
139+
|-------|----------|--------|----------|
140+
| nano-banana | `gemini-2.5-flash-image` | **Stable (GA)** | Reliable fallback, broad availability |
141+
| nano-banana-2 | `gemini-3.1-flash-image-preview` | Preview | Speed-optimized production workflows |
142+
| nano-banana-pro | `gemini-3-pro-image-preview` | Preview | Highest quality, best text rendering |
143+
144+
**IMPORTANT**: The `-image` and `-image-preview` suffixed models are DIFFERENT model IDs from the base text models. `gemini-2.5-flash` (text) does NOT support image generation output — you must use `gemini-2.5-flash-image` (with the `-image` suffix). The standard `models.list` API endpoint may not show preview models; their absence does not mean they are unavailable for `generateContent` calls.
145+
131146
---
132147

133148
## Image Generation Parameters
@@ -139,7 +154,7 @@ gemini_config:
139154
POST https://generativelanguage.googleapis.com/v1beta/models/{model_id}:generateContent
140155
```
141156
142-
Where `{model_id}` is the configured model (default: `gemini-3-pro-image-preview`).
157+
Where `{model_id}` is the configured model. Try the fallback chain in order: `gemini-3-pro-image-preview` first (highest quality), then `gemini-3.1-flash-image-preview`, then `gemini-2.5-flash-image` (GA stable). On a 404 or model-not-found error, move to the next model in the chain.
143158
144159
**Request Headers**:
145160
```

scripts/extract-infographic-data.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1042,10 +1042,24 @@ def compute_risk_funnel(tier, severity, threats_content, artifacts,
10421042
# --- T031: Missing enrichments ---
10431043
missing_enrichments = _compute_missing_enrichments(artifacts)
10441044

1045+
# --- Score-based risk metrics from compensating-controls Section 1 ---
1046+
risk_metrics = {
1047+
"risk_reduction": None,
1048+
"inherent_score": None,
1049+
"residual_score": None,
1050+
"control_coverage_pct": None,
1051+
}
1052+
if cc_data:
1053+
risk_metrics["risk_reduction"] = cc_data.get("risk_reduction")
1054+
risk_metrics["inherent_score"] = cc_data.get("inherent_score")
1055+
risk_metrics["residual_score"] = cc_data.get("residual_score")
1056+
risk_metrics["control_coverage_pct"] = cc_data.get("control_coverage_pct")
1057+
10451058
return {
10461059
"funnel_tiers": funnel_tiers,
10471060
"reduction_percentages": reduction_percentages,
10481061
"missing_enrichments": missing_enrichments,
1062+
**risk_metrics,
10491063
}
10501064

10511065

@@ -1521,7 +1535,14 @@ def main():
15211535
# Build template-specific data
15221536
template_data = {}
15231537
if args.template == "baseball-card":
1524-
template_data = {"risk_weights": risk_weights}
1538+
# Risk metrics from compensating-controls Section 1 (Tier 1 only)
1539+
template_data = {
1540+
"risk_weights": risk_weights,
1541+
"risk_reduction": cc_data.get("risk_reduction") if cc_data else None,
1542+
"inherent_score": cc_data.get("inherent_score") if cc_data else None,
1543+
"residual_score": cc_data.get("residual_score") if cc_data else None,
1544+
"control_coverage_pct": cc_data.get("control_coverage_pct") if cc_data else None,
1545+
}
15251546
elif args.template == "system-architecture":
15261547
arch_overlay = compute_architecture_overlay(scope, findings, tier, heat_map)
15271548
template_data = {
@@ -1538,6 +1559,10 @@ def main():
15381559
"funnel_tiers": funnel["funnel_tiers"],
15391560
"reduction_percentages": funnel["reduction_percentages"],
15401561
"missing_enrichments": funnel["missing_enrichments"],
1562+
"risk_reduction": funnel.get("risk_reduction"),
1563+
"inherent_score": funnel.get("inherent_score"),
1564+
"residual_score": funnel.get("residual_score"),
1565+
"control_coverage_pct": funnel.get("control_coverage_pct"),
15411566
}
15421567
elif args.template == "maestro-stack":
15431568
# Per-layer finding summaries: up to 2 top findings per layer

scripts/tachi_parsers.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,13 +647,36 @@ def parse_compensating_controls_md(content: str) -> dict:
647647
"controls": [],
648648
"coverage_summary": {"total-found": 0, "total-partial": 0, "total-missing": 0},
649649
"severity": {"critical": 0, "high": 0, "medium": 0, "low": 0, "note": 0, "total": 0},
650+
"risk_reduction": None, # e.g. 22.9
651+
"inherent_score": None, # e.g. 270.3
652+
"residual_score": None, # e.g. 208.5
653+
"control_coverage_pct": None, # e.g. 26.0 (Found percentage)
650654
}
651655

652656
if not content or not content.strip():
653657
return result
654658

655659
lines = content.split("\n")
656660

661+
# ---- Section 1: Executive Summary risk metrics ----
662+
# Parse: **Risk Reduction**: 270.3 inherent -> 208.5 residual (**22.9%** reduction)
663+
rr_match = re.search(
664+
r"\*\*Risk Reduction\*\*:\s*([\d.]+)\s*inherent\s*->\s*([\d.]+)\s*residual\s*\(\*\*([\d.]+)%\*\*",
665+
content,
666+
)
667+
if rr_match:
668+
result["inherent_score"] = float(rr_match.group(1))
669+
result["residual_score"] = float(rr_match.group(2))
670+
result["risk_reduction"] = float(rr_match.group(3))
671+
672+
# Parse: **Coverage**: 26% Found | 34% Partial | 40% Missing
673+
cov_match = re.search(
674+
r"\*\*Coverage\*\*:\s*([\d.]+)%\s*Found",
675+
content,
676+
)
677+
if cov_match:
678+
result["control_coverage_pct"] = float(cov_match.group(1))
679+
657680
# ---- Section 4: Recommendations (parse first to merge into findings) ----
658681
recommendations = {} # threat_id -> recommendation text
659682
sec4_start = None

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@
9696
}
9797
],
9898
"template_data": {
99+
"control_coverage_pct": 23.5,
100+
"inherent_score": 214.5,
101+
"residual_score": 156.8,
102+
"risk_reduction": 26.9,
99103
"risk_weights": [
100104
{
101105
"annotation": "4 High + 5 Medium + 2 Low findings",

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
}
9797
],
9898
"template_data": {
99+
"control_coverage_pct": 23.5,
99100
"funnel_tiers": [
100101
{
101102
"count": 34,
@@ -122,6 +123,7 @@
122123
"tier": 3
123124
}
124125
],
126+
"inherent_score": 214.5,
125127
"missing_enrichments": [],
126128
"reduction_percentages": [
127129
{
@@ -140,6 +142,8 @@
140142
"to_tier": 3
141143
}
142144
],
145+
"residual_score": 156.8,
146+
"risk_reduction": 26.9,
143147
"risk_weights": [
144148
{
145149
"annotation": "4 High + 5 Medium + 2 Low findings",

0 commit comments

Comments
 (0)