{% if render_mode != 'fragment' %} {% if domain %}{% endif %} Version Comparison - Finite State Report {% include "_design_system.html" %} {% include "_console_shell.html" %} {% include "_echarts_ready.html" %} {% endif %} {% if render_mode != 'fragment' %} {% endif %} {% import "_console_macros.html" as fs with context %} {# Truncation note for a table that shipped only `limit` of `total` rows: names how many were left out and which CSV/XLSX export lists them all (an HTML-only run must be re-run with CSV or XLSX output to get it). #} {% macro vc_more_note(total, limit, export_name, colspan) %} {% if limit is not none and total > limit %} {{ total - limit }} more not shown — every row is in the {{ export_name }} export (CSV or XLSX output). {% endif %} {% endmacro %} {# Status-change table — shared by the latest-pair panel and each changelog step. Rows: change_type (reopened|closed|reclassified), display_id, severity, component_name, component_version, status_baseline, status_current, title. Rows past `cap` hide behind the show-more toggle (fragments show every row: no JS to toggle); rows past `limit` are not shipped at all (vc_more_note). #} {% macro vc_status_table(rows, table_id, cap=15, limit=none) %} {% set shown = rows[:limit] if limit is not none else rows %} {% for row in shown %} {% endfor %} {% if shown | length > cap and render_mode != 'fragment' %} {% endif %} {{ vc_more_note(rows | length, limit, "Status Changes", 6) }}
Change ID Severity Component Status Title
{{ row.change_type | title }} {{ row.display_id if row.display_id else '—' }} {{ row.severity }} {{ row.component_name }}{% if row.component_version %} {{ row.component_version }}{% endif %} {{ row.status_baseline | replace('_', ' ') | title }}→{{ row.status_current | replace('_', ' ') | title }} {{ row.title if row.title else '—' }}
{% endmacro %} {# Defensive context defaults — defined OUTSIDE the gate so any downstream reference (status_bar, topbar) is safe in both branches. The non-empty branch overwrites `result` below. #} {% set result = transform_result | default({}) %} {% if not result is mapping %}{% set result = {} %}{% endif %} {# Chrome (topbar) renders in both branches — it's identifying info for the page, not data-conditional. #} {% if render_mode != 'fragment' %} {{ fs.topbar( crumbs=["Finite State", "Version Comparison"], meta=[ {"label": "Domain", "value": domain or "—"}, {"label": "Projects", "value": (result.get('project_count', 0)) | string}, {"label": "Filter", "value": (project_label if project_label else (metadata.folder_filter if metadata is defined and metadata.folder_filter else "—"))}, {"label": "Generated", "value": generated_at or "—"}, ], controls=[], ) }} {% endif %}
{# Data gate. The non-empty branch sets richer locals + renders the per-project tree; the {% else %} branch renders the empty-state guidance card. `
` is opened OUTSIDE this gate so both branches sit inside well-formed chrome — round-2 multi-review M1-1 / M2-1 caught the prior version's orphan `
` bug. #} {# Page head renders in BOTH branches so empty-portfolio runs still get the H1 + subtitle (round-2 M1-4). #}

Version Comparison

Per-project progression of findings across versions; fixed + new counts per pair; finding status changes; component churn; externally-driven CVE changes.

{% if transform_result and transform_result.projects and transform_result.projects | length > 0 %} {% set result = transform_result %} {% set kpi = result.kpi | default({}) %} {% set projects = result.projects | default([]) %} {# Partial-report banner — top-level `partial_report` and `failed_version_names` (not under transform_result). #} {% if partial_report %}
⚠ Partial report: {{ failed_version_names | length }} version{{ 's' if failed_version_names | length != 1 else '' }} could not be fetched ({{ failed_version_names | join(", ") }}). These appear as placeholders below.
{% endif %} {# External CVE changes not looked up — say so, so "no Externally changed panel" is never read as "nothing changed externally". #} {% set ext_lookup = result.get('external_changes_lookup', 'done') %} {% if ext_lookup == 'skipped' %}

External CVE changes were not looked up for this run (--no-external-changes or the web toggle "Look up external CVE changes" turned off): no Externally changed findings, New-finding notes or External Changes export.

{% elif ext_lookup == 'unavailable' %}

External CVE changes were not looked up for this run (no API access): no Externally changed findings, New-finding notes or External Changes export.

{% elif ext_lookup == 'incomplete' %}

Some external CVE-change lookups failed (see the run log), so Externally changed findings, New-finding notes and the External Changes export may be incomplete.

{% endif %} {# Aggregate KPI row — nested baseline→current shape per _aggregate_kpi (`total_findings.baseline/current/delta/pct`). The fs.kpi_cell macro takes a single label + value; render the arrow form (baseline → current) inside the value so the existing --sev-low/--sev-critical dot still signals improvement vs regression. #} {% set tf = kpi.total_findings | default({'baseline': 0, 'current': 0, 'delta': 0, 'pct': 0.0}) %} {% set cf = kpi.critical_findings | default({'baseline': 0, 'current': 0, 'delta': 0, 'pct': 0.0}) %} {% set hf = kpi.high_findings | default({'baseline': 0, 'current': 0, 'delta': 0, 'pct': 0.0}) %} {% set cp = kpi.components | default({'baseline': 0, 'current': 0, 'delta': 0, 'pct': 0.0}) %} {# Use `.kpi-row` (from _design_system.html) — the spaced grid designed for `.kpi-card` children. The compact `.kpi-bar` (from _console_shell.html) is intended for `.kpi-cell` children and would squash these baseline→current delta cards in standalone HTML/PDF. Round-1 multi-review M1-1 caught the double-class regression. #}
Total Findings
{{ tf.baseline }} → {{ tf.current }}
{% if tf.delta > 0 %}+{% endif %}{{ tf.delta }} ({% if tf.delta > 0 %}+{% endif %}{{ tf.pct }}%)
Critical
{{ cf.baseline }} → {{ cf.current }}
{% if cf.delta > 0 %}+{% endif %}{{ cf.delta }}
High
{{ hf.baseline }} → {{ hf.current }}
{% if hf.delta > 0 %}+{% endif %}{{ hf.delta }}
Components
{{ cp.baseline }} → {{ cp.current }}
{% if cp.delta > 0 %}+{% endif %}{{ cp.delta }}
Fixed (latest)
{% if kpi.fixed_count is none %}—{% else %}{{ kpi.fixed_count | default(0) }}{% endif %}
New (latest)
{% if kpi.new_count is none %}—{% else %}{{ kpi.new_count | default(0) }}{% endif %}
Status changes (latest)
{% if kpi.status_changed_count is none %}—{% else %}{{ kpi.status_changed_count | default(0) }}{% endif %}
{# Per-project sections. #} {% for proj in projects %} {% set proj_idx = loop.index %} {% set is_pair = proj.get('is_pair_comparison', False) %} {% set body_id = fragment_scope_class ~ '-vc-project-body-' ~ proj_idx %} {% set chart_id = fragment_scope_class ~ '-vc-chart-' ~ proj_idx %}
{# In fragment mode ` {% include '_action_buttons.html' %} {% endif %}