← Files VeraARCHIVED FILE
modules/bandi-agevolazioni/scripts/dossier_report.py
17.3 KB · Oct 4, 2026 · 12:28 UTC
"""Present validated dossier records as a local, source-linked review report."""
from __future__ import annotations
import json
from decimal import Decimal, InvalidOperation
from html import escape
from pathlib import Path
from typing import Any, Iterable
from urllib.parse import quote
__all__ = ["render_dossier_html"]
LABELS_PATH = Path(__file__).resolve().parents[1] / "assets" / "dossier-labels.json"
CSS = """
:root{color-scheme:light;--ink:#172b43;--muted:#526477;--blue:#125b94;--rule:#dce3ea}
*{box-sizing:border-box}html{scroll-behavior:auto}body{margin:0;background:white;color:var(--ink);font:16px/1.6 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
main{max-width:1100px;margin:auto;padding:44px 56px 72px}a{color:var(--blue);text-underline-offset:3px}a:focus-visible,summary:focus-visible{outline:3px solid #1889c9;outline-offset:4px}
header{border-top:5px solid var(--blue);padding-top:26px;margin-bottom:24px}.brand{font-weight:700;letter-spacing:.08em;font-size:13px;text-transform:uppercase;color:var(--blue)}h1{font-size:38px;line-height:1.15;letter-spacing:-.035em;margin:12px 0 20px}h2{font-size:24px;line-height:1.3;margin:0 0 16px}h3{font-size:18px;line-height:1.4;margin:10px 0}h4{font-size:16px;margin:16px 0 8px}p{margin:0 0 14px;overflow-wrap:anywhere}.subtitle{font-size:20px;max-width:850px}.notice,.muted{color:var(--muted)}.notice{font-size:14px}.status{font-weight:650;color:var(--blue);margin-top:16px}nav{display:flex;flex-wrap:wrap;gap:12px 25px;padding:17px 0;border-block:1px solid var(--rule);margin:24px 0 32px}nav a{text-decoration:none;font-size:14px;font-weight:600}
section{margin-top:36px;padding-top:30px;border-top:1px solid var(--rule)}section:first-of-type{padding-top:0;border-top:0}.lead{font-size:18px;max-width:900px}.scope{display:grid;grid-template-columns:minmax(130px,1fr) minmax(0,3fr);gap:10px 24px;margin:0}dt{color:var(--muted)}dd{margin:0;overflow-wrap:anywhere}table{border-collapse:collapse;width:100%;table-layout:fixed}th{text-align:left;font-size:13px;color:var(--muted);font-weight:650;padding:12px 10px;border-bottom:2px solid var(--rule)}td{vertical-align:top;border-bottom:1px solid var(--rule);padding:15px 10px;overflow-wrap:anywhere}.cost-table th:first-child{width:36%}.cost-table th:nth-child(2){width:19%}.cost-table th:nth-child(3){width:25%}.amount{font-variant-numeric:tabular-nums;white-space:nowrap}.small{font-size:13px}details{border-bottom:1px solid var(--rule);padding:14px 0}summary{cursor:pointer;line-height:1.5;font-weight:600;overflow-wrap:anywhere}summary .code{font-weight:400;font-size:12px;color:var(--muted);display:block;margin:0 0 3px 17px}details>div{padding:14px 0 4px 20px}td details{border:0;padding:8px 0 0}td details>div{padding:10px 0 0}td summary{font-size:13px;font-weight:500}.record{margin:0 0 22px}.record:last-child{margin-bottom:0}.meta{display:flex;gap:8px 24px;flex-wrap:wrap;font-size:13px;color:var(--muted);margin:10px 0}.refs{font-size:13px;margin:8px 0 16px}.refs a{margin-right:12px}blockquote{margin:12px 0;padding:8px 18px;border-left:3px solid #aec7dc;background:#f7f9fb;white-space:pre-wrap;overflow-wrap:anywhere}.narrative{white-space:pre-wrap}code,pre{font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere}pre{padding:12px;background:#f7f9fb}ul{padding-left:20px}.sources article{padding:12px 0 22px}footer{margin-top:40px;padding-top:20px;border-top:1px solid var(--rule);font-size:13px;color:var(--muted)}
@media(max-width:700px){main{padding:24px 20px 40px}h1{font-size:30px}.scope{grid-template-columns:1fr;gap:2px}dd{margin-bottom:12px}th,td{padding:10px 5px;font-size:13px}.amount{white-space:normal}.cost-table th:first-child{width:30%}.cost-table th:nth-child(2){width:22%}.cost-table th:nth-child(3){width:25%}}
@media print{main{max-width:none;padding:0;font-size:10pt}nav{display:none}h1{font-size:26pt}h2{font-size:16pt}section{margin-top:20px;padding-top:18px}details,article{break-inside:avoid}a{color:inherit}body{color:black}.notice{font-size:9pt}}
"""
def _text(value: object) -> str:
if value is None or value == "":
return "—"
if isinstance(value, (dict, list)):
return json.dumps(value, ensure_ascii=False, indent=2)
return str(value)
def _e(value: object) -> str:
return escape(_text(value), quote=True)
def _anchor(value: object) -> str:
return "record-" + quote(str(value), safe="")
def _money(value: object, currency: object, language: str) -> str:
if value in (None, ""):
return "—"
try:
amount = Decimal(str(value))
if not amount.is_finite():
return _text(value)
number = f"{amount:,.2f}"
except InvalidOperation:
return _text(value)
if language in {"it", "de", "es"}:
number = number.translate(str.maketrans({",": ".", ".": ","}))
elif language == "fr":
number = number.translate(str.maketrans({",": "\u202f", ".": ","}))
return f"{number} {currency or ''}".strip()
def render_dossier_html(
*,
intake: dict[str, Any],
sources: dict[str, Any],
workbench: dict[str, Any],
run_state: dict[str, Any],
audit: dict[str, Any],
) -> str:
"""Render recorded content without inferring eligibility or review decisions."""
translations = json.loads(LABELS_PATH.read_text(encoding="utf-8"))
requested_language = str(run_state.get("language", "it")).lower().split("-")[0]
language = requested_language if requested_language in translations else "en"
labels: dict[str, str] = translations[language]
def label(value: object) -> str:
return labels.get(str(value), _text(value))
def refs(values: Iterable[object]) -> str:
links = " ".join(
f'<a href="#{_anchor(value)}">{_e(value)}</a>' for value in values
)
return f'<p class="refs">{links}</p>' if links else ""
def paragraph(value: object, class_name: str = "") -> str:
return f'<p class="{class_name}">{_e(value)}</p>'
def metadata(item: dict[str, Any]) -> str:
values = [
f"{labels[heading]}: {label(item[key])}"
for key, heading in (
("readiness", "readiness"),
("review_status", "review"),
)
if item.get(key)
]
return (
'<div class="meta">'
+ "".join(f"<span>{_e(v)}</span>" for v in values)
+ "</div>"
)
def section(key: str, body: str) -> str:
return f'<section id="{key}"><h2>{_e(label(key))}</h2>{body}</section>'
def detail(
item: dict[str, Any], id_key: str, title: object, body: str, *, suffix: str = ""
) -> str:
return (
f'<details id="{_anchor(item[id_key])}{suffix}"><summary>'
f'<span class="code">{_e(item[id_key])}</span>{_e(title)}</summary>'
f"<div>{body}</div></details>"
)
def empty() -> str:
return paragraph(labels["empty"], "muted")
application, applicant, project = (
intake[key] for key in ("application", "applicant", "project")
)
body = [
'<header><p class="brand">Vera</p>',
f'<h1>{_e(labels["title"])}</h1>',
paragraph(application.get("title"), "subtitle"),
paragraph(applicant.get("legal_name")),
paragraph(labels["review_notice"], "notice"),
paragraph(label(workbench["dossier"]["disposition"]), "status"),
'</header><nav aria-label="Dossier">',
*[
f'<a href="#{key}">{_e(labels[key])}</a>'
for key in (
"summary",
"costs",
"documents",
"requirements",
"narratives",
"sources",
)
],
"</nav>",
section(
"summary",
paragraph(workbench.get("case_summary") or labels["not_provided"], "lead"),
),
]
scope_rows = [
("question", intake.get("professional_question")),
("reference_date", intake.get("reference_date")),
("project", project.get("title")),
(
"requested_amount",
_money(project.get("requested_amount"), project.get("currency"), language),
),
]
scope = (
'<dl class="scope">'
+ "".join(
f"<dt>{_e(labels[key])}</dt><dd>{_e(value)}</dd>"
for key, value in scope_rows
)
+ "</dl>"
)
body.append(section("scope", scope))
costs = []
for item in workbench.get("expenses", []):
cost_refs = refs(
[*item.get("requirement_ids", []), *item.get("source_ids", [])]
)
costs.append(
f'<tr id="{_anchor(item["expense_id"])}"><td>{_e(item.get("description"))}'
f'<details><summary>{_e(labels["reason"])}</summary><div>'
f'{paragraph(item.get("rationale"))}{cost_refs}</div></details></td>'
f'<td class="amount">{_e(_money(item.get("amount"), item.get("currency"), language))}</td>'
f'<td>{_e(label(item.get("outcome")))}</td>'
f'<td>{_e(label(item.get("review_status")))}{paragraph(label(item.get("readiness")), "small muted")}</td></tr>'
)
cost_table = (
'<table class="cost-table"><thead><tr>'
+ "".join(
f'<th scope="col">{_e(labels[key])}</th>'
for key in ("cost", "amount", "assessment", "review")
)
+ "</tr></thead><tbody>"
+ "".join(costs)
+ "</tbody></table>"
)
body.append(
section(
"costs",
paragraph(labels["proposed_note"], "muted")
+ (cost_table if costs else empty()),
)
)
documents = [
f'<article class="record" id="{_anchor(item["document_id"])}"><h3>{_e(item.get("title"))}</h3>'
+ metadata(item)
+ paragraph(item.get("rationale"))
+ refs([*item.get("requirement_ids", []), *item.get("material_source_ids", [])])
+ "</article>"
for item in workbench.get("document_checklist", [])
]
body.append(section("documents", "".join(documents) or empty()))
requirements = []
for requirement in workbench.get("requirements", []):
comparisons = []
for item in workbench.get("assessments", []):
if item["requirement_id"] != requirement["requirement_id"]:
continue
comparisons.append(
f'<article class="record" id="{_anchor(item["assessment_id"])}">'
f'<h3>{_e(label(item["outcome"]))}</h3>{metadata(item)}'
f'{paragraph(item.get("rationale"))}{refs(item.get("fact_ids", []))}</article>'
)
evidence = "".join(
refs([ref["source_id"]])
+ paragraph(ref.get("locator"), "small")
+ f'<blockquote>{_e(ref.get("excerpt"))}</blockquote>'
for ref in requirement.get("source_refs", [])
)
content = metadata(requirement)
content += "".join(comparisons) or paragraph(labels["no_assessment"])
content += f'<h4>{_e(labels["expected"])}</h4>'
content += paragraph("; ".join(requirement.get("expected_evidence", [])))
content += f'<h4>{_e(labels["excerpt"])}</h4>{evidence}'
requirements.append(
detail(requirement, "requirement_id", requirement.get("statement"), content)
)
body.append(section("requirements", "".join(requirements) or empty()))
narratives = [
f'<article class="record" id="{_anchor(item["narrative_id"])}"><h3>{_e(item.get("prompt"))}</h3>'
+ paragraph(item.get("draft"), "narrative")
+ metadata(item)
+ refs([*item.get("requirement_ids", []), *item.get("fact_ids", [])])
+ "</article>"
for item in workbench.get("narratives", [])
]
body.append(section("narratives", "".join(narratives) or empty()))
consistency = [
detail(
item,
"check_id",
item.get("question"),
paragraph(label(item.get("outcome")))
+ paragraph(item.get("rationale"))
+ metadata(item)
+ refs([*item.get("fact_ids", []), *item.get("source_ids", [])]),
)
for item in workbench.get("consistency_checks", [])
]
body.append(section("consistency", "".join(consistency) or empty()))
for group in ("issues", "informational_notes"):
issues = [
f'<article class="record" id="{_anchor(item["issue_id"])}">'
+ paragraph(item.get("detail"))
+ paragraph(label(item.get("status")), "muted")
+ metadata(item)
+ refs(item.get("related_ids", []))
+ "</article>"
for item in workbench.get("issues", [])
if (item.get("severity") == "informational")
== (group == "informational_notes")
]
if issues or group == "issues":
body.append(
section(
group, "".join(issues) or paragraph(labels["no_issues"], "muted")
)
)
sources_html = []
for source in sources.get("sources", []):
sources_html.append(
f'<article id="{_anchor(source["source_id"])}"><h3>{_e(source.get("title"))}</h3>'
+ paragraph(source.get("issuer"), "muted")
+ f'<p><code>{_e(source.get("path"))}</code></p>'
+ detail(
source,
"source_id",
labels["source_details"],
f"<pre>{_e(source)}</pre>",
suffix="-details",
)
+ "</article>"
)
sources_body = "".join(sources_html)
body.append(
section(
"sources", '<div class="sources">' + (sources_body or empty()) + "</div>"
)
)
facts = "".join(
detail(
item,
"fact_id",
item.get("field_code"),
f'<pre>{_e(item.get("value"))}</pre>'
+ metadata(item)
+ refs(item.get("source_ids", [])),
)
for item in workbench.get("facts", [])
)
forms = "".join(
detail(
item,
"field_id",
item.get("label"),
paragraph(item.get("proposed_value"))
+ paragraph(item.get("rationale"))
+ metadata(item)
+ f"<pre>{_e(item)}</pre>",
)
for item in workbench.get("form_fields", [])
)
authority = workbench["authority_simulation"]
authority_body = paragraph(label(authority.get("overall_outcome")))
if authority.get("reviewer_perspective"):
authority_body += paragraph(authority["reviewer_perspective"])
authority_body += "".join(
detail(
item,
"check_id",
item.get("question"),
paragraph(label(item.get("outcome")))
+ paragraph(item.get("rationale"))
+ metadata(item)
+ refs(item.get("related_ids", [])),
)
for item in authority.get("checks", [])
)
limits = [
*workbench["dossier"].get("limitations", []),
*(
(
labels["mechanical_validation_limit"]
if value
== "Mechanical validation does not establish source authority, legal interpretation, eligibility, cost admissibility, or filing readiness."
else value
)
for value in audit.get("limitations", [])
),
]
controls = paragraph(labels["technical_note"])
controls += (
"<ul>"
+ "".join(
f'<li><a href="{filename}">{_e(filename)}</a></li>'
for filename in (
"review_dossier.md",
"case_intake.json",
"source_register.json",
"application_workbench.json",
"intelligence_register.json",
"review_log.json",
"validation_audit.json",
"run_state.json",
)
)
+ "</ul>"
)
for key, content in (
("facts", facts or empty()),
("forms", forms or empty()),
("authority", authority_body),
("limitations", "".join(paragraph(value) for value in limits) or empty()),
):
controls += f"<h3>{_e(labels[key])}</h3>{content}"
body.append(
section(
"technical",
f'<details><summary>{_e(labels["technical"])}</summary><div>{controls}</div></details>',
)
)
action_rows = [
(key, flag)
for key, flag in (
("portal", "portal_actions_performed"),
("signature", "signature_actions_performed"),
("submission", "submission_actions_performed"),
)
]
actions = (
'<dl class="scope">'
+ "".join(
f'<dt>{_e(labels[key])}</dt><dd>{_e(labels["recorded" if run_state[flag] else "not_recorded"])}</dd>'
for key, flag in action_rows
)
+ "</dl>"
)
body.append(section("actions", actions))
body.append(f'<footer>{_e(labels["review_notice"])}</footer>')
return (
f'<!doctype html><html lang="{language}"><head><meta charset="utf-8">'
'<meta name="viewport" content="width=device-width,initial-scale=1">'
'<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'">'
f'<title>{_e(labels["title"])} · {_e(applicant.get("legal_name"))}</title><style>{CSS}</style>'
"</head><body><main>" + "".join(body) + "</main></body></html>\n"
)
SHA-256: bae92d8c75a748ac1069bccc40e0e6bbcb7c0896144961773153110e5e479ce3