synthesize
Thesis report synthesis — turn a thesis's markdown artifacts into its letter-size thesis-report.html, then OPTIMIZE it against real page renders. Four-step loop — pack (deterministic Python bundle of
Files2
View on GitHubSKILL.mdplugins/vertical-plugins/scenarios/skills/agentii/synthesize/SKILL.md
GitHubagentii.synthesize
The single-point HTML generation step (spec 046 Q46–Q50): ONE thesis-report.html
per thesis, authored from the markdown artifacts and then optimized against
real renders. Analysis skills emit markdown only (Q49); the report is assembled
here, at the synthesis step, after the cross-stock synthesis
(_cross/*_synthesis.md) exists.
When to run
- After the synthesis tasks complete (the
_cross/deliverable is written). - When
convergeemits anhtml_stalefinding (sources or template moved). - On explicit request, or once at thesis completion (Q50 regeneration triggers).
The loop (four steps + optimize)
# 0. RESOLVE the kit root — contracts/kit-root.md. The kit's scripts do NOT ship with
# the skills, so never assume the CWD is the checkout (`cd agentii-investment-
# intelligence` was this skill's old step 0, and it only worked for a user sitting
# in the checkout). This resolver fails loudly rather than guessing:
KIT=""
for c in "${AGENTII_KIT_ROOT:-}" \
"$(cat "$HOME/.claude/skills/agentii/.kit-root" 2>/dev/null)" \
"${CLAUDE_PLUGIN_ROOT:-}"; do
[ -n "$c" ] && [ -f "$c/scripts/agentii_cmd.py" ] && { KIT="$c"; break; }
done
if [ -z "$KIT" ]; then d="$PWD"; while [ "$d" != "/" ]; do
[ -f "$d/scripts/agentii_cmd.py" ] && { KIT="$d"; break; }; d="$(dirname "$d")"; done; fi
[ -n "$KIT" ] || { echo "agentii kit not found — see contracts/kit-root.md" >&2; exit 1; }
# 1. PACK — deterministic bundle of every source, verbatim (no timestamps),
# PLUS report/metrics.json (per-ticker key_metrics/conclusions/counts —
# machine-ready numbers for KPI tiles and kpi_trend charts, RAW values):
python3 "$KIT/scripts/synthesize_report.py" pack --thesis <theses/{nnn}-{slug}>
# 2. AUTHOR — read <thesis>/report-input.md (verbatim sources + citations)
# and <thesis>/report/metrics.json (numbers). Write
# <thesis>/report/content.html — the report's actual content is YOUR judgment.
# 2b. SCORE (required — spec 058 FR-065). The blocking gate decides what TEXT can
# decide; it cannot see whether the report is any good, and "passes the gate"
# must never be read as "reads well". Run the scorer for the RUBRIC and the
# MEASURED evidence — the judgement is yours, the measurement is not:
python3 "$KIT/scripts/score_report_readability.py" <thesis-dir>/report/content.html
# Then read the report and record your total (five criteria, 1–5 each → 1–25)
# in <thesis-dir>/report/readability.json:
# {"score": 21, "previous": 18, "explanation": ""}
# `previous` is the last RELEASED report's score; omit it (or write null) for a
# first report, which sets its own baseline — no score is chosen in advance.
# A score BELOW `previous` requires `explanation`, and `assemble` REFUSES the
# drop without one: FR-065's consequence is that a regression is explained in
# the deliverable, and a score recorded without consequence is the defect
# FR-040 forbids. A LOW score is not refused — only an unexplained regression.
# The score is rendered on the cover beside the pins (T091), so it has a named
# consumer: the human reviewer.
# 3. ASSEMBLE — validate + inject + gate (advisories on stderr are guidance,
# the hard gates are silent until they fail):
python3 "$KIT/scripts/synthesize_report.py" assemble --thesis <thesis-dir> --check-only # fit loop
python3 "$KIT/scripts/synthesize_report.py" assemble --thesis <thesis-dir> # deliver
# 4. RENDER — Chrome headless → letter PDF → per-page PNGs + manifest.
# --keep-pdf is REQUIRED for delivery: without it the PDF is written to a temp dir,
# used for the PNGs, and DELETED. The deliverable is HTML **and** PDF.
python3 "$KIT/scripts/render_report.py" render --thesis <thesis-dir> --keep-pdf
# 5. OPTIMIZE (required, not optional): READ the PNGs page by page, in batches
# of 3–4. Fix real problems the estimator cannot see — clipped tables, ugly
# URL wrapping, weak density, orphan headings, oversized tiles. Edit
# content.html → re-assemble → re-render until EVERY page is visually clean.
Deliverables
Two files, both at the thesis root — report the absolute paths when you finish:
<thesis-dir>/thesis-report.html<thesis-dir>/thesis-report.pdf
The PDF is the deliverable, not a QA by-product. render's docstring frames it as the
"visual QA loop" because the PNGs are what the optimize pass reads — but --keep-pdf
is what turns its --print-to-pdf output into the artifact a user actually sends to
someone, and it belongs beside the HTML rather than in the report/pages/ scratch
folder with the PNGs.
Renders never gate CI (Chrome/poppler may be absent) — the author's own visual
pass is the gate. assemble --check-only remains the estimator fallback; a
missing render is a hard stop for delivery, not a silent skip. render --verify
also checks each PNG is letter-width at the requested dpi.
content.html contract (the assembler hard-gates every rule)
You author only the page sequence — a fragment, never a document:
- One or more
<section class="page">…</section>blocks, no nesting. The cover is page 1 and is template-owned: do not author it — the assembler fills kicker / title / claim / pins / universe / generated / TOC, and injects the running sheet head/foot, page marks (NN / TOTAL) and corner registration marks into every page. - Fragment only —
<!DOCTYPE>,<html>,<head>,<body>,<style>,<script>are rejected. Element whitelist:section h1 h2 h3 p ul ol li table thead tbody tr th td b strong i em code a div span br hr blockquote. - No
<img>and no id startingcover-or equal tostale-bar. - Reserved classes — never emit these (assembler chrome):
sheet-head,sheet-foot,reg,cover,page-mark,disclaimer. A collision is a hard validation error, not a warning. The disclaimer page is the newest of these (Q139): one legal tail page, numbered last, injected bysynthesize_report.build_htmlfromtemplates/disclaimer.md— the single source for that text. Do not author it, do not paraphrase it, and do not put a disclaimer in a page of your own. It is deliberately absent from the contents list: it is a legal tail, not a chapter. - Citation gate (anti-fabrication): every viewer link you emit must be
copied verbatim from
report-input.md— the pack's links have the form…/v/{TICKER}/{citation_id}/{N}on the agentii viewer. The assembler rejects any{ticker}/{citation_id}pair it cannot find in the sources, with an offender list. Never invent a citation_id; every[FACT]number you surface keeps its citation link.metrics.jsoncarries NO citation ids — never build viewer links from tiles. Citations that only exist as bare text in the pack (keyword scans likeISRG × ect75 × page1) may be rendered as plain text — do not turn them into fake links. - Charts (Q48) are tokens, not images:
<div data-chart="kpi_trend" data-spec='{"x":["FY2024","FY2025","H1 2026"],"y":[77.7,128.3,96.3]}' data-height="160"></div>data-specis single-quoted JSON (no apostrophes inside).kpi_trendis for non-price structured series (capex trajectories, revenue series) built frommetrics.jsonmulti-period keys.football_field {labels,lows,highs},peer_bars {labels,values},scatter {x,y},scenario_tree {edges}remain market-data-only.data-heightis your page-budget claim — the overflow estimator counts it exactly, so keep it honest. Research-only theses (market-data stage none) emit onlykpi_trend, if anything.
Component rules (v0.3.0 design system)
- KPI tiles: the executive-summary page REQUIRED to open with a
.stat-rowof 2–4 thesis-level tiles (e.g. artifacts count · key-metrics count · pillars supported · GPT-3.5 window). Per-ticker sections carry 2–4 ticker tiles. Structure:div.stat > div.num + div.lbl + div.sub. Numbers frommetrics.json, formatted per style.md:$28.5B(not$28,476M; one decimal for billions, none for millions),12.4%,+12.4%,14.2x. - Badges — two orthogonal axes, never confused:
- taxonomy (FR-092):
[FACT]→.badge-fact,[DEDUCTED]→.badge-deducted,[VIEW]→.badge-view— use on evidence-table rows and claim prose; - verdicts:
supported→.badge-supported,indeterminate→.badge-indeterminate,refuted→.badge-refuted— every pillar verdict carries one.
- taxonomy (FR-092):
- Timeline: the capability-timeline page uses
.timelinewith.tl-itemblocks —.tl-phaseholds the mono phase label, the prose keeps the synthesis's wording with its citations. - Kickers: every page opens with a
.sec-kicker(mono editorial label, e.g.THE CORPUS · 24 ARTIFACTS), never the heading text repeated.
Letter-fit rules (author to these numbers)
A letter page holds ~40 lines of prose at 11pt — the --check-only gate, the
render PNGs and the in-browser red ⚠ overflow outline are the enforcement.
Author conservatively:
- ≤ 40 prose lines per page (paragraphs + bullets, combined).
- ≤ 18 table rows per page when cells wrap ≤ 2 lines each; a citation cell counts as 2 lines. Split long tables across pages by ticker or period.
- A
.stat-rowcosts ~4 lines; a.tl-itemcosts ~3; a chart token costs itsdata-heighthonestly. - Headings cost budget: one
h1/h2+ 2–3 short paragraphs, or a table block — not both, unless the table is small.
What a good report contains
- Executive Summary — the synthesis's own prose, condensed to the operative sentences; tile row; the headline callout (capability timeline band + confidence) kept intact; verdict list with badges.
- Pillar verdicts — every pillar: verdict badge, falsifier result, the best evidence bullets with citations (verbatim from the pack).
- Capability timeline — the synthesis §2 content as a styled
.timeline(it is the thesis's headline output). - Evidence — the cross-ticker table (§3) split to fit,
[FACT]badges on rows, citation links inline; negative findings ("no MTBF disclosure anywhere") are evidence too — keep them. - Coverage gaps — the synthesis §4 items; the mechanical rollup (entry counts by skill) may appear as one small table.
- Data-quality flags (§5) and contract compliance (§6), condensed.
- Nothing from the raw YAML/JSON of sources — the pack and metrics.json are your inputs, not your output; prose and structure are yours, facts and citations are verbatim.
Quality checklist (MUST, before delivery)
- Exec-summary tile row present (2–4 tiles).
- All pillar verdicts carry colored badges; taxonomy badges on evidence rows.
- Capability timeline styled (
.timeline/.tl-item/.tl-phase). - Evidence tables follow style.md (Metric → Current → Prior → YoY → Citation) with inline citation links on every fact.
.sec-kickeron every page.- No empty pages, no orphan headings at page bottoms.
- No raw markdown leakage (
**, backtick fences,###). - Tier-0
--check-onlypass ANDrenderpage-count == section-count AND every PNG visually verified clean (no clipped tables, no red overflow outlines, sane density). - Readability scored (FR-065).
report/readability.jsoncarries the 1–25 total fromscore_report_readability.py, and a score below the previous release's carries anexplanation.assemblerefuses a drop without one, so a missing explanation is a hard stop, not a note. The score appears on the cover beside the pins and is the ONLY readability signal a reader sees — the blocking gate's pass is not a substitute, and the two are deliberately reported separately so neither can be mistaken for the other.
Regeneration discipline (Q50)
The assembled report embeds sources_hash (all markdown sources) +
template_version; converge flags html_stale when they drift. Re-run the
loop above when that finding appears — never hand-edit thesis-report.html.