$npx -y skills add eph5xx/tweakidea --skill ti-reportJinja2 templates + CSS + data contract for TweakIdea evaluation reports
| 1 | Reference files for rendering styled evaluation reports via `scripts/render_report.py`. |
| 2 | Not directly invoked by Claude — consumed by the script layer only. |
| 3 | |
| 4 | --- |
| 5 | |
| 6 | ## Files |
| 7 | |
| 8 | | File | Purpose | |
| 9 | |------|---------| |
| 10 | | `template.html.j2` | Jinja2 HTML template. Rendered with `autoescape=True` to prevent XSS from idea text / LLM prose. Notion-style layout with sticky topbar, hero stats, dim-grid, collapsible per-dimension detail rows, tabbed research highlights, reach-potential callout, and dark-mode toggle. | |
| 11 | | `template.md.j2` | Jinja2 markdown template. Rendered with `autoescape=False` so raw markdown symbols (`>`, `—`, `|`) pass through unescaped. Mirrors the HTML section order but stays plain-text: lists, headings, tables — no tabs or collapsibles. | |
| 12 | | `styles.css` | All CSS rules extracted from the sample mock verbatim. Read at render time by `render_report.py` and inlined into a `<style>` tag in `report.html` (portable single-file artifact). Uses CSS custom properties for theming; `[data-theme="dark"]` flips the palette. | |
| 13 | | `SKILL.md` | This file. Documents files, rendering contract, and data extraction contract. | |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## Dual-Environment Rendering Contract |
| 18 | |
| 19 | `scripts/render_report.py` builds **two** Jinja2 environments sharing the same `FileSystemLoader`: |
| 20 | |
| 21 | ```python |
| 22 | from jinja2 import Environment, FileSystemLoader, select_autoescape |
| 23 | |
| 24 | loader = FileSystemLoader(str(TI_REPORT_DIR)) |
| 25 | |
| 26 | html_env = Environment( |
| 27 | loader=loader, |
| 28 | autoescape=True, # force escaping for this env |
| 29 | trim_blocks=True, |
| 30 | lstrip_blocks=True, |
| 31 | ) |
| 32 | |
| 33 | md_env = Environment( |
| 34 | loader=loader, |
| 35 | autoescape=False, |
| 36 | trim_blocks=True, |
| 37 | lstrip_blocks=True, |
| 38 | ) |
| 39 | ``` |
| 40 | |
| 41 | **Pitfall 2 warning:** Do NOT use `select_autoescape(["html", "htm"])` — `template.html.j2` ends in `.j2` and would not be matched. The `html_env` must use `autoescape=True` directly to guarantee HTML escaping regardless of filename. |
| 42 | |
| 43 | The CSS is passed as a template variable `css`: |
| 44 | |
| 45 | ```python |
| 46 | data["css"] = (TI_REPORT_DIR / "styles.css").read_text() |
| 47 | ``` |
| 48 | |
| 49 | The HTML template inlines it: `<style>{{ css | safe }}</style>`. |
| 50 | |
| 51 | **`|safe` is used only for the `css` variable.** The CSS contains `>` combinators (`.rh-tabs > input`), `"` attribute selectors (`[data-panel="user"]`), and `&` characters; without `|safe`, Jinja2's autoescape mangles them into `>` / `"` / `&` and breaks the CSS-only tab switcher plus many other selectors. The CSS comes from a static file on disk loaded by `render_report.py` — it is never user-supplied, so the `|safe` bypass is secure. |
| 52 | |
| 53 | All *other* interpolated prose (idea text, narrative fields, assumption text, research text, key findings, score explanations) flows through either autoescape or the `linkify` filter (which escapes first, then wraps URLs). The only raw HTML beyond `css` is the inline SVG icon sprite and theme-toggle script, which are trusted template literals, not data. |
| 54 | |
| 55 | --- |
| 56 | |
| 57 | ## Data Contract |
| 58 | |
| 59 | Template variables assembled by `render_report.py` from the run artifact family: |
| 60 | |
| 61 | | Variable | Source JSON | Schema | |
| 62 | |----------|------------|--------| |
| 63 | | `timestamp` | arg `--timestamp` or `datetime.now()` | string | |
| 64 | | `css` | `skills/ti-report/styles.css` (file, not JSON) | string | |
| 65 | | `idea` | `idea.json` | `schemas/idea.json` | |
| 66 | | `numbers` | `numbers.json` | `schemas/numbers.json` | |
| 67 | | `strengths_weaknesses` | `strengths-weaknesses.json` | `schemas/strengths-weaknesses.json` | |
| 68 | | `next_steps` | `next-steps.json` | `schemas/next-steps.json` | |
| 69 | | `potential` | `potential.json` | `schemas/potential.json` | |
| 70 | | `version` | `version.json` | `schemas/version.json` | |
| 71 | | `hypotheses` | `hypotheses.json` (optional) | `schemas/hypotheses.json` | |
| 72 | | `assumptions` | `assumptions.json` (optional) | `schemas/assumptions.json` | |
| 73 | | `research` | `research.json` (optional) | `schemas/research.json` | |
| 74 | | `dimensions` | `dimensions/*.json` (all 14, keyed by slug) | `schemas/dimension-evaluation.json` | |
| 75 | |
| 76 | Key lookups used in templates: |
| 77 | |
| 78 | - `idea.codename`, `idea.icon`, `idea.subtitle` — all optional; template renders sensible fallbacks when absent (codename → "Evaluation", icon → 📋, subtitle → `idea.problem`). |
| 79 | - `numbers.rankings[i]` — each entry now carries `weight` (0–1 fraction of registry weight), used to render `{weight*100}%` in the dim-grid and the per-dimension collapsibles. |
| 80 | - `numbers.rankings[i].slug` → key into `dimensions` dict for `dimensions[r.slug].key_finding` and `dimensions[r.slug].score_explanation`. |
| 81 | - `numbers.verdict_bucket` → one of `GO|PIVOT|STOP` — used to select the verdict pill color (`green`, `yellow`, `red`) and the `.verdict-word` class. |
| 82 | - `numbers.verdict_label` → constant string in the form `"BUCKET — descriptor"` (e.g. `"PIVOT — Promising, address weak areas"`). The hero-stats meta line slices off the bucket prefix and renders the descriptor tail next to the styled `.verdict-word`. There is no LLM-authored verd |