{# Compound-report shell. Context vars (assembled by fs_report.compound_assembler): - ``compound`` — the CompoundRecipe being rendered. - ``cover_html`` — pre-rendered cover-page HTML (or empty string). - ``toc_entries`` — list[{anchor, title, number}] for the TOC. - ``show_toc`` — bool; gates the TOC block. - ``show_page_numbers`` — bool; gates the page-number footer. - ``compound_libs`` — deduped list of chart-library tokens (echarts, marked, dompurify, chartjs, datalabels) drawn from each child's Recipe.chart_libraries. - ``section_blocks`` — list[str]; one HTML block per child (already contains the section divider + fragment body OR a failure-callout placeholder). - ``classification`` — text for cover badge + page footer (may be empty). - ``tokens_inline_css`` — verbatim contents of static/css/tokens.css. The compound shell is the home for compound-wide CSS (cover, page layout, section dividers). Per-section CSS travels INSIDE each section block — fragment_extractor scopes it to ``.fs-section-`` so it can't bleed across siblings. See the compound-reports design spec § 5 (Compound Assembler) and § 6 (Visual Design). THEME — LIGHT-ONLY for META-COMPARE only (Fix I / M1-7, scoped R3 M3-2): the redesigned meta-compare surfaces ship light-only (no .mc-* dark variants). _design_system.html transitively pulls _theme_init.html, whose 'auto' branch can read a persisted dark theme from another report's localStorage and activate the dark tokens (:root[data-theme="dark"] in tokens.css), degrading these surfaces. We defend the on-screen meta-compare HTML two ways: 1. data-theme="light" is set directly on in markup (gated to meta-compare below), so the document is light even before/without JS and regardless of inherited state. 2. theme='light' is passed into this render context (see compound_assembler .assemble, ALSO gated to meta-compare) so _theme_init.html takes its EXPLICIT branch — it re-asserts data-theme="light" and never consults localStorage / ?theme= / prefers-color-scheme. The JS therefore cannot flip this layout to dark. NON-meta compounds get NEITHER gate, so they keep the normal auto/persisted theme behavior — forcing light on every bundle was a cross-feature regression. PDF is light regardless (pdf_mode pins the server theme). #} {{ resolved_title }} {# Recipe shell styles. Standalone renders include these inside their own gated ; fragment renders strip the document shell so the CSS doesn't reach the bundle. The compound shell loads them here so every section's content (with classes like .topbar / .panel / .kpi-bar from console recipes and .briefing chrome from briefing recipes) styles correctly inside the bundle. (B1.4 follow-up: fragment-mode visual parity.) ``_design_system.html`` transitively includes ``_tokens_inline.html`` plus ``_theme_init.html`` — we do NOT also include _tokens_inline directly above, since that would emit the :root token block twice in every compound bundle's . (Round-1 multi-review M1-4, M2-3, M3-2 — 3/3 consensus.) Class-collision audit (round-1 multi-review M2-2 / M3-1): of the 23 user-facing recipes, 22 use ``_console_shell.html`` (briefing bundle expects those styles) and 1 — ``executive_summary.html`` — uses ``_briefing_shell.html``. We audited class-name overlap between executive_summary's emitted markup and console_shell's selectors: zero conflicts (briefing classes are .briefing / .hero / .stats / .stat / .deck / .distribution / .methodology / .exec-summary-sections / .data-table — console classes are .panel / .grid / .main / .sidebar / .table-panel / .topbar / .kpi-bar / .band etc.). Including console_shell at bundle scope is safe for the briefing recipe; the alternative (per-recipe dynamic shell selection) is significantly more complex without a real conflict to motivate it. #} {% include "_design_system.html" %} {% include "_console_shell.html" %} {# Chart-readiness coordinator + per-engine wrappers. Idempotent: the fsBeaconAcquire/Release counter spans every child's charts, so window.fsReportReady only flips true once *every* chart has painted. Including both partials is safe — each does an internal "if already defined, return" guard. #} {% include "_chart_ready.html" %} {% include "_echarts_ready.html" %} {# Per-child chart libraries (union of children's chart_libraries). #} {% include "_compound_libs.html" %} {# ====================== ICON SPRITE (inlined Lucide, offline-safe) ====================== Document-global / defs so #i-* ids resolve from any section (exec band + comparison section templates reference these via ). Copied verbatim from the meta-compare design handoff prototype. #} {% if cover_html %} {{ cover_html|safe }} {% endif %}
{# ====================== EXEC "SYNC PLAN" ====================== Front matter, before the TOC. Gated to meta-compare bundles only. Every value is bound from the deterministic ``action_plan`` dict (computed in compound_assembler._compute_action_plan) + the ``verdict``/per-facet summary dicts (cd/fd/ld/td) — NO Jinja-side math, NO hand-written prose. Leader→laggard direction, severity-weighted headline figures, the ranked steps + owners + effort enum are all decided in Python; the template only composes fixed strings around the slots. (Spec §6 Exec "Sync Plan".) #} {% if verdict and verdict.is_meta_compare and action_plan %} {% set cd = verdict.cd %}{% set fd = verdict.fd %}{% set ld = verdict.ld %}{% set td = verdict.td %} {% set ap = action_plan %}
Sync Plan
{# Severity-weighted headline — a lead acts on severity, not a raw count. Raw total demoted to the subtitle. Three mutually-exclusive cases (fixed strings around slots, no Jinja math): 1. port_total > 0 → severity-weighted port headline (code fixes lead). 2. else if verdict.port_triage > 0 → triage-only: lead with the propagation framing — no "fixes" language, since a no_engineering plan ports VEX decisions, not code (Fix D / M1-4 / M1-9). 3. else → nothing to do; scopes are aligned. #} {# Degraded banner — shown whenever ANY comparison facet failed to render (a FailedSection the rendered-only summaries/rows never see). It sits ABOVE the headline and fires independent of which headline branch wins, so a surviving port- OR triage-led Sync Plan still warns that work from the failed facet may be missing — not only the empty/aligned case (Fix 2 / M1-1). #} {% if degraded %}
A comparison facet failed to render — this Sync Plan may be incomplete; work from the failed facet is not reflected below. See the section errors.
{% endif %} {% if ap.port_total > 0 %}

Port {{ ap.crit }} Critical + {{ ap.high }} High fixes to {{ ap.laggard }}.

{{ ap.leader }} leads: {{ ap.port_total }} fix{{ '' if ap.port_total == 1 else 'es' }} resolved there are still open in {{ ap.laggard }}{{ ap.crit + ap.high }} Critical or High. Each ranked step below is one component bump or a no-engineering triage sync.

{% elif verdict.port_triage > 0 %}

Sync {{ verdict.port_triage }} triage decision{{ '' if verdict.port_triage == 1 else 's' }} to {{ ap.laggard }} — no engineering.

{# Neutral sync framing, NOT "to closure" (Fix 4 / M2-3): a propagated triage decision can be EXPLOITABLE (assessed but still actionable) — it syncs the assessment, it doesn't necessarily close the finding. #}

{{ ap.leader }} has already triaged {{ verdict.port_triage }} finding{{ '' if verdict.port_triage == 1 else 's' }} that {{ ap.laggard }} has not yet assessed. No code change — copy the dispositions. The single step below is a no-engineering triage sync.

{% elif not degraded %} {# Genuine all-clear: no port/triage work AND no facet failed to render. (When degraded with no surviving work, the banner above is the message — we do NOT print a false "aligned" headline.) #}

Scopes are aligned.

Nothing to port from {{ ap.leader }} to {{ ap.laggard }} — no resolved fixes or triage decisions are sitting open on the other side. Review the facets below for context.

{% endif %} {# Parity / no-false-comfort line: closing the gap reaches parity, not safety. Fixed caption; renders when the shared backlog exceeds the leader-direction port count (computed in Python). #} {% if ap.needs_action_in_both_count > ap.port_count %}
Closing this gap brings the two scopes to parity — not to safety. Both still carry {{ ap.needs_action_in_both_count }} finding{{ '' if ap.needs_action_in_both_count == 1 else 's' }} open on both sides, on their own remediation track.
{% endif %} {# ---- THE ACTION PLAN — ranked, severity-weighted (steps come pre-sorted and pre-ranked from Python; the loop only composes the fixed copy). ---- #} {% if ap.steps %}
Do this, in order
{% for step in ap.steps %} {% set sc = step.sev_counts %} {% set is_low = (sc.CRITICAL|default(0)) == 0 and (sc.HIGH|default(0)) == 0 %}
{{ step.rank }}

{%- if step.effort_kind == 'no_engineering' -%} Apply {{ ap.leader }}'s triage decisions — no engineering {%- elif step.fix_target -%} Upgrade {{ step.component }} {{ step.fix_target }} {%- else -%} Rebuild {{ step.component }} {%- endif -%}

{# "Syncs" for the no-engineering triage step (it propagates decisions — a synced status can be EXPLOITABLE, not a removal); "Clears" only for code fixes, which genuinely remove findings (Fix 4 / M2-3). #}

{{ 'Syncs' if step.effort_kind == 'no_engineering' else 'Clears' }} {{ step.clears }} of {{ ap.port_total if step.effort_kind != 'no_engineering' else step.total }} {%- set tags = [] -%} {%- if sc.CRITICAL|default(0) %}{% set _ = tags.append(sc.CRITICAL ~ ' Critical') %}{% endif -%} {%- if sc.HIGH|default(0) %}{% set _ = tags.append(sc.HIGH ~ ' High') %}{% endif -%} {%- if sc.MEDIUM|default(0) %}{% set _ = tags.append(sc.MEDIUM ~ ' Medium') %}{% endif -%} {%- if sc.LOW|default(0) %}{% set _ = tags.append(sc.LOW ~ ' Low') %}{% endif -%} {%- if tags %} — {{ tags | join(', ') }}{% endif -%}.

{# Owner chips — render only when the step has non-empty owners. #} {% if step.owners %}
Affects {% for owner in step.owners %}{{ owner | short_scope }}{% endfor %}
{% endif %}
{{ sc.CRITICAL|default(0) }}C · {{ sc.HIGH|default(0) }}H · {{ sc.MEDIUM|default(0) }}M · {{ sc.LOW|default(0) }}L {% if step.effort_kind == 'no_engineering' %}No engineering {% elif step.effort_kind == 'component_bump' %}1 component bump {% else %}1 component rebuild{% endif %}
{{ step.clears }}{% if step.effort_kind != 'no_engineering' %} / {{ ap.port_total }}{% endif %}
{# A no_engineering step PROPAGATES leader triage decisions; a propagated status can be EXPLOITABLE (assessed but still actionable — a sync, not a closure/suppression). Use neutral sync wording, not "suppressed". "fixes cleared" stays only where a code fix genuinely removes findings. (Fix 4 / M2-3.) #}
{{ 'decisions synced' if step.effort_kind == 'no_engineering' else 'fixes cleared' }}
{% if sc.CRITICAL|default(0) %}{% endif %} {% if sc.HIGH|default(0) %}{% endif %} {% if sc.MEDIUM|default(0) %}{% endif %} {% if sc.LOW|default(0) %}{% endif %}
{% endfor %}
{% endif %} {# ---- Facet index cards — ordered by (has_action desc, action_count desc, name). License is always last (has_action false). Built as a sortable list of fixed dicts, then sorted; status chip = fixed label. ---- #} {% set fct_finding = { 'order': 1, 'name': 'Findings', 'num': '02', 'icon': 'i-shield', 'anchor': 'fs-section-finding-diff', 'has_action': (fd.fixed_left_open_right_count|default(0) if verdict.left_leads else fd.fixed_right_open_left_count|default(0)) > 0, 'action_count': (fd.fixed_left_open_right_count|default(0) if verdict.left_leads else fd.fixed_right_open_left_count|default(0)), 'metric': (ap.crit + ap.high), 'unit': 'crit+high to port', 'note': verdict.port_fixes ~ ' total · ' ~ (fd.needs_action_in_both_count|default(0)) ~ ' open on both', 'chip': ('Action' if verdict.port_fixes > 0 else 'Aligned') } %} {# Triage card chip keys on PROPAGATION (the Action Plan only acts on verdict.port_triage), NOT on divergence (Fix E / M1-10): propagation → Action (matches the plan + this card's has_action); divergence-only → Context (review, not a mechanical apply step); neither → Aligned. 'unit' is "to sync", NOT "to suppress" (Fix 4 / M2-3): a propagated triage decision can be EXPLOITABLE — assessed but still actionable — so the facet ports DECISIONS, it doesn't suppress findings. #} {% set fct_triage = { 'order': 2, 'name': 'Triage', 'num': '04', 'icon': 'i-list-checks', 'anchor': 'fs-section-triage-status-diff', 'has_action': verdict.port_triage > 0, 'action_count': verdict.port_triage, 'metric': verdict.port_triage, 'unit': 'to sync', 'note': (td.status_divergence_count|default(0)) ~ ' status conflict' ~ ('' if (td.status_divergence_count|default(0)) == 1 else 's') ~ ' · no engineering', 'chip': ('Action' if verdict.port_triage > 0 else ('Context' if td.status_divergence_count|default(0) > 0 else 'Aligned')) } %} {% set fct_component = { 'order': 3, 'name': 'Components', 'num': '01', 'icon': 'i-package', 'anchor': 'fs-section-component-diff', 'has_action': (verdict.laggard_only + (cd.version_skew_count|default(0))) > 0, 'action_count': (verdict.laggard_only + (cd.version_skew_count|default(0))), 'metric': verdict.laggard_only, 'unit': 'extra in ' ~ (verdict.laggard | short_scope), 'note': (cd.shared_count|default(0)) ~ ' shared · ' ~ (cd.version_skew_count|default(0)) ~ ' version skew', 'chip': ('Context' if (verdict.laggard_only + (cd.version_skew_count|default(0))) > 0 else 'Aligned') } %} {# License card 'chip' uses the ALL-FOUR-zero condition — match license_diff.html's §03 aligned strip exactly (R3 M2-2): the two-term shorthand hid added/removed copyleft obligations when only copyleft_additions_count / copyleft_removals_count were nonzero. #} {% set fct_license = { 'order': 4, 'name': 'License', 'num': '03', 'icon': 'i-scale', 'anchor': 'fs-section-license-diff', 'has_action': false, 'action_count': 0, 'metric': (ld.license_changes_count|default(0)), 'unit': 'differences', 'note': (ld.copyleft_additions_count|default(0)) ~ ' copyleft added · ' ~ (ld.copyleft_removals_count|default(0)) ~ ' removed', 'chip': ('Aligned' if (ld.license_changes_count|default(0)) == 0 and (ld.copyleft_additions_count|default(0)) == 0 and (ld.copyleft_removals_count|default(0)) == 0 and (ld.copyleft_changes_count|default(0)) == 0 else 'Context') } %} {# Sort: has_action desc, action_count desc, then the deterministic original order (license always last via its has_action=false + order=4). #} {% set facets = [fct_finding, fct_triage, fct_component, fct_license] | sort(attribute='order') | sort(attribute='action_count', reverse=true) | sort(attribute='has_action', reverse=true) %}
{% endif %} {# Skip the TOC for bundles with 2 or fewer sections — a 2-entry TOC leaves 7/8 of an A4 page empty, costing a whole page of front matter before the first content section. The cover already names the bundle; listing two sections is overhead. Bundles with 3+ sections show the TOC. Authors can still force the TOC off via output.toc=False. #} {% if show_toc and toc_entries and toc_entries|length >= 3 %} {% endif %} {% for block in section_blocks %} {{ block|safe }} {% endfor %}
{# /.fs-compound-content #} {# Meta-compare worklist tail reveal — progressive enhancement only. The report is fully legible with JS disabled (all rows are in the DOM; print forces .mc-row-hidden visible). Ported from the design handoff's meta-compare.js (the [data-more] handler only — theme is handled by the shell; the tweaks-panel + annotation toggles are prototype-only and omitted). A button with data-more="" toggles .expanded on that table to reveal the collapsed tail rows. #} {# Action-button gating: _action_buttons.html ships a DOMContentLoaded listener that hides every .action-btn / .action-cell when the page is NOT served via http://localhost (the only context in which Jira integration + Remediation Package generation work). Triage Prioritization includes this partial in its standalone path; the include is gated by render_mode != 'fragment' there so it doesn't leak into fragment output. The compound shell re-includes it ONCE at bundle level so the gating JS runs even when the bundle is opened from file:// (which a saved compound HTML always is) or rendered to PDF via Playwright. Without this, Jira / Remediation buttons would show up in every compound that bundled a recipe carrying action cells, despite the bundle having no proxy to call through. (B1.4 follow-up.) #} {% include "_action_buttons.html" %} {# Footer template — Chromium's page.pdf(footer_template=...) extracts this element's innerHTML at PDF time and renders it on every page. All CSS inline (Chromium's footer context has no stylesheet access). The compound assembler passes pdf_footer_template_id="compound-footer" to PDFRenderer.render_html(). When NEITHER classification text NOR page numbers are configured, emit just so PDFRenderer.render_html()'s display-header- footer check (which treats as the empty sentinel) correctly disables Chromium's header/footer mode. Otherwise an essentially-empty footer band would still reserve vertical space on every page. (PR #100 round-1 multi-review M1-9.) #}