← Files VeraARCHIVED FILE

modules/adeguati-assetti/scripts/construction_intake.py

10.7 KB · Oct 2, 2026 · 00:29 UTC

↓ Download file

"""Offline interview handoff: versioned UTF-8 text, never executable instructions."""

from __future__ import annotations

import base64
import html
import json
from pathlib import Path

from construction_core import digest, fields, require, text

__all__ = ["import_intake", "parse_markdown", "render_form", "render_markdown"]

INTAKE_SCHEMA = "vera.assetti_intake.v1"
PREFIX = "# Scheda di visita per Vera\n\n```assetti-intake-json\n"


def render_markdown(payload: dict) -> str:
    """Keep delimiter characters escaped in JSON and indent human-readable answers."""
    encoded = json.dumps(
        payload, ensure_ascii=False, indent=2, allow_nan=False
    ).replace("`", "\\u0060")
    lines = [
        PREFIX + encoded + "\n```\n",
        "Le risposte sono dichiarazioni da verificare; non approvano voti o esclusioni.\n",
    ]
    for answer in payload["answers"]:
        lines.append(f"## {answer['question_id']}\n")
        for value in (
            answer["question"],
            answer["status"],
            answer["original"],
            answer["notes"],
            ", ".join(answer["attachment_refs"]),
        ):
            lines.append("\n".join("    " + line for line in value.split("\n")) + "\n")
    return "\n".join(lines)


def parse_markdown(content: str) -> dict:
    """Reject ambiguous or malformed versions; the readable text must match JSON."""
    require(content.startswith(PREFIX), "Not a supported Vera intake Markdown")
    encoded, separator, _ = content[len(PREFIX) :].partition("\n```\n")
    require(bool(separator), "Missing structured intake terminator")
    payload = json.loads(encoded)
    require(
        isinstance(payload, dict) and payload.get("schema_version") == INTAKE_SCHEMA,
        "Unsupported intake schema",
    )
    fields(
        payload,
        "practice_id",
        "client_id",
        "engagement_id",
        "session_id",
        "date",
        "declared_author",
        "questionnaire_version",
    )
    require(
        type(payload.get("revision")) is int and payload["revision"] >= 1,
        "Invalid intake revision",
    )
    require(
        payload.get("mode") in {"html", "text", "reviewed_transcript"},
        "Unsupported intake mode",
    )
    require(isinstance(payload.get("answers"), list), "Missing interview answers")
    seen = set()
    for answer in payload["answers"]:
        fields(answer, "question_id", "question", "status")
        require(answer["question_id"] not in seen, "Duplicate intake question")
        seen.add(answer["question_id"])
        require(
            answer["status"] in {"answered", "to_verify", "unknown"},
            "Invalid intake answer state",
        )
        require(
            isinstance(answer.get("original"), str)
            and isinstance(answer.get("notes"), str),
            "Answer and notes must be text",
        )
        require(
            isinstance(answer.get("attachment_refs"), list)
            and all(isinstance(x, str) for x in answer["attachment_refs"]),
            "Invalid attachment references",
        )
        require(
            isinstance(answer.get("criterion_ids"), list),
            "Missing answer criterion bindings",
        )
        require(
            not any(
                k in answer
                for k in (
                    "score",
                    "qualification_review",
                    "na_approved",
                    "professional_decision",
                )
            ),
            "Intake cannot approve assessments",
        )
    require(
        content == render_markdown(payload),
        "Readable answers differ from structured data; re-export the edited draft",
    )
    return payload


def import_intake(
    state: dict, payload: dict, *, source_id: str, actor: str, at: str
) -> None:
    """Consume a bound original once, with explicit conflicts for parallel edits."""
    from construction_core import _put

    require(
        payload["practice_id"] == state["case_id"]
        and payload["client_id"] == state["client_id"]
        and payload["engagement_id"] == state["engagement_id"],
        "Intake belongs to another practice",
    )
    require(
        payload["questionnaire_version"] == state["catalog"]["methodology_version"],
        "Questionnaire version changed",
    )
    require(source_id in state["evidence"], "Import the original interview file first")
    sessions = state.setdefault("intake_sessions", {})
    sid = payload["session_id"]
    previous = sessions.get(sid)
    current_digest = digest(payload)
    if previous:
        if previous["payload_sha256"] == current_digest:
            return
        require(
            payload["revision"] > previous["revision"],
            "Conflicting intake revision; compare both drafts",
        )
        require(
            payload.get("parent_sha256") == previous["payload_sha256"],
            "Conflicting intake parent; compare both drafts",
        )
    known = {c["id"] for c in state["catalog"]["criteria"]}
    for row in payload["answers"]:
        require(
            all(cid in known for cid in row["criterion_ids"]),
            "Unknown intake criterion",
        )
        answer = {
            "id": sid + "/" + row["question_id"],
            "question_id": row["question_id"],
            "question": row["question"],
            "original": row["original"] or "Non fornito",
            "summary": "",
            "speaker": payload["declared_author"],
            "date": payload["date"],
            "mode": payload["mode"],
            "status": row["status"],
            "criterion_ids": row["criterion_ids"],
            "evidence_refs": [source_id],
            "attachment_refs": row["attachment_refs"],
            "notes": row["notes"],
            "unresolved_attachment_refs": [
                r for r in row["attachment_refs"] if r not in state["evidence"]
            ],
        }
        _put(state, "answers", answer, actor, at)
    sessions[sid] = {
        "unobserved_parent_sha256": (
            previous.get("unobserved_parent_sha256")
            if previous
            else payload.get("parent_sha256")
        ),
        "revision": payload["revision"],
        "payload_sha256": current_digest,
        "source_id": source_id,
        "payload": payload,
    }


def render_form(state: dict, *, questions: list[dict] | None = None) -> str:
    """Emit a standalone page; no remote assets, storage, microphone or network."""
    sessions = list(state.get("intake_sessions", {}).values())
    saved = sessions[-1]["payload"] if sessions else None
    if (
        saved
        and saved["questionnaire_version"] != state["catalog"]["methodology_version"]
    ):
        saved = None
    if questions is None and saved:
        questions = [
            {
                "id": r["question_id"],
                "question": r["question"],
                "criterion_ids": r["criterion_ids"],
            }
            for r in saved["answers"]
        ]
    if questions is None:
        questions = [
            {
                "id": "OPENING",
                "question": "Raccontami come funziona l’azienda e chi segue le attività principali.",
                "criterion_ids": [],
            }
        ]
        questions.extend(
            {"id": c["id"], "question": c["question"], "criterion_ids": [c["id"]]}
            for c in state["catalog"]["criteria"]
        )
    require(
        isinstance(questions, list) and bool(questions), "Provide at least one question"
    )
    known = {c["id"] for c in state["catalog"]["criteria"]}
    seen = set()
    for question in questions:
        fields(question, "id", "question")
        require(question["id"] not in seen, "Duplicate question ID")
        seen.add(question["id"])
        require(
            isinstance(question.get("criterion_ids"), list)
            and all(cid in known for cid in question["criterion_ids"]),
            "Unknown question criterion",
        )
    initial = None
    if saved:
        initial = json.loads(json.dumps(saved))
        previous = {row["question_id"]: row for row in initial["answers"]}
        for question in questions:
            if question["id"] in previous:
                original = previous[question["id"]]
                require(
                    original["question"] == question["question"]
                    and original["criterion_ids"] == question["criterion_ids"],
                    "A changed question needs a new ID; preserve its original answer",
                )
            else:
                initial["answers"].append(
                    {
                        "question_id": question["id"],
                        "question": question["question"],
                        "criterion_ids": question["criterion_ids"],
                        "original": "",
                        "status": "unknown",
                        "notes": "",
                        "attachment_refs": [],
                    }
                )
        questions = [
            {
                "id": row["question_id"],
                "question": row["question"],
                "criterion_ids": row["criterion_ids"],
            }
            for row in initial["answers"]
        ]
    config = {
        "schema_version": INTAKE_SCHEMA,
        "practice_id": state["case_id"],
        "client_id": state["client_id"],
        "engagement_id": state["engagement_id"],
        "questionnaire_version": state["catalog"]["methodology_version"],
        "entity_name": state["entity_name"],
        "questions": questions,
        "saved_intake": saved,
        "initial_intake": initial,
        "resume_summary": state["cursor"].get("summary", ""),
        "next_step": state["cursor"].get("next_step", ""),
    }
    template = (
        Path(__file__).resolve().parents[1] / "assets/construction-intake.html"
    ).read_text(encoding="utf-8")
    encoded = (
        json.dumps(config, ensure_ascii=False)
        .replace("<", "\\u003c")
        .replace(">", "\\u003e")
        .replace("&", "\\u0026")
    )
    module = Path(__file__).resolve().parents[1]
    candidates = [
        module / "vendor/modules/courseware/assets/InstrumentSans-Regular.ttf",
        module.parent.parent
        / "vendor/modules/courseware/assets/InstrumentSans-Regular.ttf",
        module.parent
        / "_shared/vendor/modules/courseware/assets/InstrumentSans-Regular.ttf",
    ]
    font = next((p for p in candidates if p.is_file()), None)
    font_face = (
        "@font-face{font-family:'Instrument Sans';font-style:normal;font-weight:100 900;src:url(data:font/ttf;base64,"
        + base64.b64encode(font.read_bytes()).decode()
        + ") format('truetype');}"
        if font
        else ""
    )
    return (
        template.replace("__CASE_TITLE__", html.escape(state["entity_name"]))
        .replace("__CONFIG_JSON__", encoded)
        .replace("__FONT_FACE__", font_face)
    )

SHA-256: cbee5c6d57da936bec28c1776772f324de89e989edb21fcec4d4df4d156a3a5c