← Files VeraARCHIVED FILE

modules/report-builder/scripts/review_successor.py

34.9 KB · Oct 5, 2026 · 18:29 UTC

↓ Download file

"""Deterministic replay of Report Builder review successor state.

This module proves that persisted application effects, material edit receipts,
and delivery status derive from the recorded review decisions and current
prepared artifacts.  It does not authenticate who recorded those decisions;
reviewer identity remains an external authority boundary.
"""

from __future__ import annotations

import hashlib
import json
import re
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Any

__all__ = ["validate_review_successor"]

SCHEMA_VERSION = "report_builder.review_successor_validation.v1"
ACTION_STATUSES = {
    "accept": "accepted",
    "reject": "rejected",
    "edit": "edited",
    "mark_unclear": "needs_evidence",
    "request_more_documents": "needs_evidence",
    "skip": "skipped",
}
FOLLOWUP_ACTIONS = frozenset({"reject", "mark_unclear", "request_more_documents"})
DECISION_REQUIRED_FIELDS = frozenset(
    {
        "item_id",
        "item_type",
        "title",
        "action",
        "status",
        "decided_at",
    }
)
DECISION_OPTIONAL_FIELDS = frozenset(
    {
        "reviewer_note",
        "edit_value",
        "requested_documents",
        "followup_context",
    }
)
UI_REQUIRED_FIELDS = frozenset(
    {
        "schema_version",
        "plugin",
        "workflow",
        "run_id",
        "review_payload_sha256",
        "decided_at",
        "decision_source",
        "review_payload_path",
        "decisions",
        "decision_count",
        "item_count",
        "status",
    }
)
UI_OPTIONAL_FIELDS = frozenset({"reviewer"})
INTEGRITY_SCHEMA = "report_builder.review_integrity.v4"
HISTORY_SCHEMA = "report_builder.review_history_entry.v2"
INTEGRITY_FIELDS = frozenset(
    {
        "schema_version",
        "run_id",
        "source_index",
        "predecessor_checkpoint",
        "protected_files",
        "payload_digests",
        "implementation_artifact_refs",
        "implementation_receipts",
        "prepared_validation",
        "physical_paths",
        "physical_directories",
        "content_sha256",
    }
)


def _clean_text(value: object) -> str:
    return str(value or "").strip()


def _canonical_sha256(value: Any) -> str:
    return hashlib.sha256(
        json.dumps(
            value,
            ensure_ascii=False,
            sort_keys=True,
            separators=(",", ":"),
            allow_nan=False,
        ).encode("utf-8")
    ).hexdigest()


def _read_object(path: Path) -> dict[str, Any]:
    payload = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(payload, dict):
        raise ValueError(f"Expected JSON object: {path.name}")
    return payload


def _require_exact_fields(
    value: Mapping[str, Any],
    required: frozenset[str],
    optional: frozenset[str] = frozenset(),
    *,
    label: str,
) -> None:
    observed = set(value)
    if not required <= observed or observed - required - optional:
        raise ValueError(f"Report Builder {label} fields are not exact.")


def _safe_item_id(value: object) -> str:
    cleaned = re.sub(r"[^A-Za-z0-9._-]+", "-", _clean_text(value))
    return cleaned.strip("-")[:80] or "item"


def _string_list(value: object, *, label: str) -> list[str]:
    if not isinstance(value, list) or not all(
        isinstance(item, str) and item.strip() for item in value
    ):
        raise ValueError(f"Report Builder {label} must be a string list.")
    if len(value) != len(set(value)):
        raise ValueError(f"Report Builder {label} contains duplicates.")
    return value


def _review_items(review_payload: Mapping[str, Any]) -> dict[str, dict[str, Any]]:
    raw_items = review_payload.get("items")
    if not isinstance(raw_items, list) or not all(
        isinstance(item, dict) for item in raw_items
    ):
        raise ValueError("Report Builder review items are malformed.")
    items = {
        _clean_text(item.get("id")): item
        for item in raw_items
        if _clean_text(item.get("id"))
    }
    if len(items) != len(raw_items):
        raise ValueError("Report Builder review item identities are not unique.")
    if review_payload.get("item_count") != len(raw_items):
        raise ValueError("Report Builder review item count is stale.")
    return items


def _validate_decision(
    decision: Mapping[str, Any],
    item: Mapping[str, Any],
    *,
    decided_at: object,
) -> None:
    _require_exact_fields(
        decision,
        DECISION_REQUIRED_FIELDS,
        DECISION_OPTIONAL_FIELDS,
        label="decision",
    )
    action = _clean_text(decision.get("action"))
    allowed = item.get("allowed_actions")
    if (
        not isinstance(allowed, list)
        or action not in ACTION_STATUSES
        or action not in allowed
        or decision.get("item_id") != item.get("id")
        or decision.get("item_type") != item.get("item_type")
        or decision.get("title") != item.get("title")
        or decision.get("status") != ACTION_STATUSES[action]
        or decision.get("decided_at") != decided_at
    ):
        raise ValueError("Report Builder decision is not review-payload-derived.")
    if action == "edit" and not _clean_text(decision.get("edit_value")):
        raise ValueError("Report Builder edit decision has no value.")
    for field in ("reviewer_note", "edit_value"):
        if field in decision and not isinstance(decision[field], str):
            raise ValueError(f"Report Builder decision {field} is malformed.")
    if "requested_documents" in decision:
        _string_list(
            decision["requested_documents"],
            label="decision requested_documents",
        )
    if "followup_context" in decision and not isinstance(
        decision["followup_context"], Mapping
    ):
        raise ValueError("Report Builder decision followup_context is malformed.")


def _validate_ui_state(
    review_payload: Mapping[str, Any],
    ui_decisions: Mapping[str, Any],
) -> list[dict[str, Any]]:
    _require_exact_fields(
        ui_decisions,
        UI_REQUIRED_FIELDS,
        UI_OPTIONAL_FIELDS,
        label="UI decision",
    )
    items = _review_items(review_payload)
    raw_decisions = ui_decisions.get("decisions")
    if not isinstance(raw_decisions, list) or not all(
        isinstance(decision, dict) for decision in raw_decisions
    ):
        raise ValueError("Report Builder UI decisions are malformed.")
    decision_ids = [_clean_text(decision.get("item_id")) for decision in raw_decisions]
    count = len(raw_decisions)
    expected_status = (
        "pending_review"
        if count == 0
        else "reviewed" if count == len(items) else "partial_review"
    )
    decided_at = ui_decisions.get("decided_at")
    if (
        ui_decisions.get("schema_version") != review_payload.get("schema_version")
        or ui_decisions.get("plugin") != review_payload.get("plugin")
        or ui_decisions.get("workflow") != review_payload.get("workflow")
        or ui_decisions.get("run_id") != review_payload.get("run_id")
        or ui_decisions.get("review_payload_sha256")
        != _canonical_sha256(review_payload)
        or ui_decisions.get("review_payload_path") != "review_payload.json"
        or not _clean_text(ui_decisions.get("decision_source"))
        or ui_decisions.get("decision_count") != count
        or ui_decisions.get("item_count") != len(items)
        or ui_decisions.get("status") != expected_status
        or len(decision_ids) != len(set(decision_ids))
        or (count == 0 and decided_at is not None)
        or (count > 0 and not _clean_text(decided_at))
    ):
        raise ValueError("Report Builder UI decision state is not rederived.")
    if "reviewer" in ui_decisions and not _clean_text(ui_decisions["reviewer"]):
        raise ValueError("Report Builder reviewer reference is malformed.")
    for decision in raw_decisions:
        item = items.get(_clean_text(decision.get("item_id")))
        if item is None:
            raise ValueError("Report Builder decision item does not exist.")
        _validate_decision(decision, item, decided_at=decided_at)
    return raw_decisions


def _item_target(item: Mapping[str, Any], *names: str) -> str | None:
    data = item.get("data")
    data_mapping = data if isinstance(data, Mapping) else {}
    for name in names:
        value = _clean_text(data_mapping.get(name))
        if value:
            return value
    return None


def _expected_effect_authority(
    decision: Mapping[str, Any],
    item: Mapping[str, Any],
    *,
    applied_at: object,
) -> dict[str, Any]:
    target_artifact = (
        _item_target(item, "target_artifact")
        or _clean_text(item.get("output_path"))
        or _item_target(item, "path")
    )
    expected: dict[str, Any] = {
        "item_id": decision["item_id"],
        "item_type": decision["item_type"],
        "title": decision["title"],
        "action": decision["action"],
        "status": decision["status"],
        "applied_at": applied_at,
        "applied": True,
        "requires_followup": decision["action"] in FOLLOWUP_ACTIONS,
        "target_artifact": target_artifact or None,
        "target_path": (
            _item_target(item, "target_path", "field_path", "field") or None
        ),
        "target_id_field": (
            _item_target(item, "target_id_field", "record_id_field") or None
        ),
        "target_record_id": (
            _item_target(item, "target_record_id", "record_id") or None
        ),
        "target_field": (_item_target(item, "target_field", "edit_field") or None),
        "target_records_key": (
            _item_target(item, "target_records_key", "records_key") or None
        ),
        "source_path": _clean_text(item.get("source_path")) or None,
    }
    for field in DECISION_OPTIONAL_FIELDS:
        if field in decision:
            expected[field] = decision[field]
    return expected


def _validate_receipt(path: Path, receipt: Mapping[str, Any]) -> None:
    content = path.read_bytes()
    if receipt.get("path") != path.name and receipt.get("path") != path.as_posix():
        raise ValueError("Report Builder successor receipt path is stale.")
    if (
        receipt.get("byte_count") != len(content)
        or receipt.get("sha256") != hashlib.sha256(content).hexdigest()
    ):
        raise ValueError("Report Builder successor receipt bytes are stale.")


def _validate_review_history(
    output_dir: Path,
    applied: Mapping[str, Any],
) -> list[str]:
    raw_paths = applied.get("review_history_paths", [])
    if (
        not isinstance(raw_paths, list)
        or not all(isinstance(path, str) for path in raw_paths)
        or len(raw_paths) != len(set(raw_paths))
    ):
        raise ValueError("Report Builder review history paths are malformed.")
    for relative_path in raw_paths:
        match = re.fullmatch(
            r"revisions/history/application__([0-9a-f]{64})\.json",
            relative_path,
        )
        if match is None:
            raise ValueError("Report Builder review history path is not canonical.")
        history = _read_object(output_dir / relative_path)
        expected_fields = {
            "schema_version",
            "archived_at",
            "predecessor_checkpoint",
            "predecessor_integrity",
            "run_intake",
            "review_payload",
            "ui_decisions",
            "applied_decisions",
            "final_artifacts",
            "content_sha256",
        }
        content = dict(history)
        digest = content.pop("content_sha256", None)
        replayed = _canonical_sha256(content)
        predecessor_checkpoint = _clean_text(history.get("predecessor_checkpoint"))
        predecessor_integrity = history.get("predecessor_integrity")
        if (
            set(history) != expected_fields
            or history.get("schema_version") != HISTORY_SCHEMA
            or not _clean_text(history.get("archived_at"))
            or re.fullmatch(r"[0-9a-f]{64}", predecessor_checkpoint) is None
            or not isinstance(predecessor_integrity, Mapping)
            or not isinstance(history.get("run_intake"), Mapping)
            or not isinstance(history.get("review_payload"), Mapping)
            or not isinstance(history.get("ui_decisions"), Mapping)
            or not isinstance(history.get("applied_decisions"), Mapping)
            or not isinstance(history.get("final_artifacts"), Mapping)
            or digest != replayed
            or match.group(1) != replayed
        ):
            raise ValueError("Report Builder review history entry is stale.")
        if (
            set(predecessor_integrity) != INTEGRITY_FIELDS
            or predecessor_integrity.get("schema_version") != INTEGRITY_SCHEMA
            or predecessor_integrity.get("content_sha256") != predecessor_checkpoint
        ):
            raise ValueError(
                "Report Builder predecessor integrity checkpoint is stale."
            )
        predecessor_content = dict(predecessor_integrity)
        predecessor_content.pop("content_sha256")
        if _canonical_sha256(predecessor_content) != predecessor_checkpoint:
            raise ValueError(
                "Report Builder predecessor integrity checkpoint is stale."
            )
        payload_digests = predecessor_integrity.get("payload_digests")
        archived_payloads = {
            "run_intake": history["run_intake"],
            "review_payload": history["review_payload"],
            "ui_decisions": history["ui_decisions"],
            "applied_decisions": history["applied_decisions"],
            "final_artifacts": history["final_artifacts"],
        }
        if (
            not isinstance(payload_digests, Mapping)
            or set(payload_digests) != set(archived_payloads)
            or any(
                payload_digests.get(name) != _canonical_sha256(value)
                for name, value in archived_payloads.items()
            )
        ):
            raise ValueError("Report Builder predecessor payload receipts are stale.")
        prior_review = history["review_payload"]
        prior_ui = history["ui_decisions"]
        prior_applied = history["applied_decisions"]
        prior_final = history["final_artifacts"]
        prior_decisions = _validate_ui_state(prior_review, prior_ui)
        source_mapping_review_required = (
            prior_applied.get("source_mapping_review_required") is True
        )
        if (
            prior_applied.get("run_id") != prior_review.get("run_id")
            or prior_applied.get("review_payload_sha256")
            != _canonical_sha256(prior_review)
            or (
                not source_mapping_review_required
                and (
                    prior_applied.get("decisions") != prior_decisions
                    or prior_applied.get("decision_count") != len(prior_decisions)
                )
            )
            or (
                source_mapping_review_required
                and (
                    prior_decisions
                    or prior_ui.get("decision_count") != 0
                    or re.fullmatch(
                        r"[0-9a-f]{64}",
                        _clean_text(
                            prior_applied.get("decision_review_payload_sha256")
                        ),
                    )
                    is None
                )
            )
            or prior_applied.get("item_count") != prior_review.get("item_count")
            or prior_final.get("run_id") != prior_review.get("run_id")
        ):
            raise ValueError("Report Builder predecessor review application is stale.")
        protected_files = predecessor_integrity.get("protected_files")
        if not isinstance(protected_files, list) or not all(
            isinstance(receipt, Mapping) for receipt in protected_files
        ):
            raise ValueError(
                "Report Builder predecessor protected receipts are malformed."
            )
        receipts_by_path = {
            _clean_text(receipt.get("path")): receipt for receipt in protected_files
        }
        required_receipts = {
            "run_intake.json",
            "review_payload.json",
            "ui_decisions.json",
            "applied_decisions.json",
            "final_artifacts.json",
        }
        if len(receipts_by_path) != len(
            protected_files
        ) or not required_receipts <= set(receipts_by_path):
            raise ValueError(
                "Report Builder predecessor protected receipts are incomplete."
            )
        outputs = prior_final.get("outputs")
        if not isinstance(outputs, list):
            raise ValueError("Report Builder predecessor final outputs are malformed.")
        for output in outputs:
            if not isinstance(output, Mapping):
                raise ValueError(
                    "Report Builder predecessor final outputs are malformed."
                )
            output_path = _clean_text(output.get("path"))
            receipt = receipts_by_path.get(output_path)
            if (
                receipt is None
                or output.get("size_bytes") != receipt.get("byte_count")
                or output.get("sha256") != receipt.get("sha256")
            ):
                raise ValueError(
                    "Report Builder predecessor material-output receipt is stale."
                )
    return raw_paths


def _validate_retained_review_paths(applied: Mapping[str, Any]) -> list[str]:
    raw_paths = applied.get("retained_review_paths", [])
    if (
        not isinstance(raw_paths, list)
        or not all(isinstance(path, str) for path in raw_paths)
        or len(raw_paths) != len(set(raw_paths))
        or any(
            re.fullmatch(
                r"revisions/(?:report__[A-Za-z0-9._-]+\.txt|"
                r"originals/report__[A-Za-z0-9._-]+\.docx)",
                path,
            )
            is None
            for path in raw_paths
        )
    ):
        raise ValueError("Report Builder retained review paths are malformed.")
    return raw_paths


def _validate_edit_effect(
    output_dir: Path,
    effect: Mapping[str, Any],
    *,
    source_mapping_changed: bool,
    numeric_present: bool,
) -> list[str]:
    target_path = _clean_text(effect.get("target_path"))
    expected_field = (
        "assigned_table" if target_path.endswith(".assigned_table") else "codex_comment"
    )
    if (
        effect.get("target_artifact") != "report.docx"
        or not re.fullmatch(
            rf"sections\.[A-Za-z0-9_]+\.{expected_field}",
            target_path,
        )
        or effect.get("artifact_update") != "native_artifact_regenerated"
        or effect.get("requires_native_regeneration") is not False
        or effect.get("native_regeneration_status") != "regenerated"
        or effect.get("terminal_application") is not True
    ):
        raise ValueError("Report Builder edit successor is not terminal.")
    expected_paths = (
        [
            "used_recipe.json",
            "report_analysis.json",
            "report_audit.json",
            "report_tables.json",
            "report_tables.xlsx",
            "report_draft.md",
            "report.docx",
        ]
        if source_mapping_changed
        else [
            "used_recipe.json",
            "report_analysis.json",
            "report_draft.md",
            "report.docx",
        ]
    )
    if numeric_present:
        expected_paths.extend(["numeric_evidence_ledger.json", "source_receipts.json"])
    if effect.get("native_regenerated_paths") != expected_paths:
        raise ValueError("Report Builder edit regenerated path set is stale.")
    item_id = _clean_text(effect.get("item_id"))
    revision_path = f"revisions/report__{_safe_item_id(item_id)}.txt"
    if effect.get("revision_artifact") != revision_path or (
        output_dir / revision_path
    ).read_text(encoding="utf-8") != effect.get("edit_value"):
        raise ValueError("Report Builder edit revision is not decision-derived.")
    receipt = effect.get("application_receipt")
    if not isinstance(receipt, Mapping):
        raise ValueError("Report Builder edit application receipt is missing.")
    expected_receipt_fields = {
        "target_path",
        "applied_value_sha256",
        "used_recipe_sha256",
        "report_analysis_sha256",
        "report_draft_sha256",
        "report_docx_sha256",
        "regenerated_outputs",
    }
    if set(receipt) != expected_receipt_fields:
        raise ValueError("Report Builder edit application receipt is not exact.")
    expected_hashes = {
        "target_path": target_path,
        "applied_value_sha256": hashlib.sha256(
            _clean_text(effect.get("edit_value")).encode("utf-8")
        ).hexdigest(),
        "used_recipe_sha256": hashlib.sha256(
            (output_dir / "used_recipe.json").read_bytes()
        ).hexdigest(),
        "report_analysis_sha256": hashlib.sha256(
            (output_dir / "report_analysis.json").read_bytes()
        ).hexdigest(),
        "report_draft_sha256": hashlib.sha256(
            (output_dir / "report_draft.md").read_bytes()
        ).hexdigest(),
        "report_docx_sha256": hashlib.sha256(
            (output_dir / "report.docx").read_bytes()
        ).hexdigest(),
    }
    if any(receipt.get(key) != value for key, value in expected_hashes.items()):
        raise ValueError("Report Builder edit application receipt is stale.")
    raw_outputs = receipt.get("regenerated_outputs")
    if not isinstance(raw_outputs, list) or len(raw_outputs) != len(expected_paths):
        raise ValueError("Report Builder regenerated receipt set is not exact.")
    for relative_path, output_receipt in zip(
        expected_paths,
        raw_outputs,
        strict=True,
    ):
        if not isinstance(output_receipt, Mapping):
            raise ValueError("Report Builder regenerated receipt is malformed.")
        if output_receipt.get("path") != relative_path:
            raise ValueError("Report Builder regenerated receipt order is stale.")
        content = (output_dir / relative_path).read_bytes()
        if (
            output_receipt.get("byte_count") != len(content)
            or output_receipt.get("sha256") != hashlib.sha256(content).hexdigest()
        ):
            raise ValueError("Report Builder regenerated receipt bytes are stale.")
    return expected_paths


def _expected_application_status(
    applied: Mapping[str, Any],
    analysis: Mapping[str, Any],
    *,
    source_mapping_changed: bool,
) -> str:
    if int(applied.get("blocker_count") or 0) > 0:
        return "blocked"
    if int(applied.get("native_regeneration_count") or 0) > 0:
        return "partial_review_applied"
    if int(applied.get("decision_count") or 0) < int(applied.get("item_count") or 0):
        return "partial_review_applied"
    effects = applied.get("effects")
    if not isinstance(effects, list):
        raise ValueError("Report Builder successor effects are malformed.")
    if any(
        isinstance(effect, Mapping)
        and effect.get("action") == "edit"
        and (
            effect.get("artifact_update") != "native_artifact_regenerated"
            or effect.get("terminal_application") is not True
            or not isinstance(effect.get("application_receipt"), Mapping)
        )
        for effect in effects
    ):
        return "partial_review_applied"
    sections = analysis.get("sections")
    if isinstance(sections, list) and any(
        isinstance(section, Mapping)
        and section.get("numeric_measure_status") == "needs_review"
        for section in sections
    ):
        return "partial_review_applied"
    if source_mapping_changed:
        return "partial_review_applied"
    return "final_ready"


def _validate_applied_state(
    output_dir: Path,
    review_payload: Mapping[str, Any],
    ui_decisions: Mapping[str, Any],
    applied: Mapping[str, Any],
    analysis: Mapping[str, Any],
    final_artifacts: Mapping[str, Any],
) -> dict[str, Any]:
    items = _review_items(review_payload)
    raw_decisions = applied.get("decisions")
    raw_effects = applied.get("effects")
    if (
        not isinstance(raw_decisions, list)
        or not all(isinstance(decision, dict) for decision in raw_decisions)
        or not isinstance(raw_effects, list)
        or not all(isinstance(effect, dict) for effect in raw_effects)
        or len(raw_decisions) != len(raw_effects)
    ):
        raise ValueError("Report Builder applied successor is malformed.")
    decision_ids = [_clean_text(decision.get("item_id")) for decision in raw_decisions]
    if len(decision_ids) != len(set(decision_ids)):
        raise ValueError("Report Builder applied decisions are not unique.")
    applied_at = applied.get("applied_at")
    if not _clean_text(applied_at):
        raise ValueError("Report Builder applied timestamp is missing.")
    for decision in raw_decisions:
        item = items.get(_clean_text(decision.get("item_id")))
        if item is None:
            raise ValueError("Report Builder applied decision item does not exist.")
        _validate_decision(
            decision,
            item,
            decided_at=decision.get("decided_at"),
        )
    source_mapping_changed = any(
        effect.get("action") == "edit"
        and re.fullmatch(
            r"sections\.[A-Za-z0-9_]+\.assigned_table",
            _clean_text(effect.get("target_path")),
        )
        for effect in raw_effects
    )
    if source_mapping_changed:
        if (
            ui_decisions.get("decision_source") != "not_collected_after_regeneration"
            or ui_decisions.get("decisions") != []
            or ui_decisions.get("status") != "pending_review"
        ):
            raise ValueError("Report Builder regenerated review state is stale.")
    elif raw_decisions != ui_decisions.get("decisions"):
        raise ValueError(
            "Report Builder applied decisions do not derive from UI decisions."
        )
    numeric_present = (output_dir / "numeric_evidence_ledger.json").is_file()
    union_regenerated_paths: set[str] = set()
    edit_effects: list[Mapping[str, Any]] = []
    for decision, effect in zip(raw_decisions, raw_effects, strict=True):
        item = items[_clean_text(decision.get("item_id"))]
        expected = _expected_effect_authority(
            decision,
            item,
            applied_at=applied_at,
        )
        authority_fields = (
            {key: value for key, value in expected.items() if key != "source_path"}
            if source_mapping_changed
            else expected
        )
        if any(effect.get(key) != value for key, value in authority_fields.items()):
            raise ValueError(
                "Report Builder application effect is not decision-derived."
            )
        if decision.get("action") == "edit":
            edit_effects.append(effect)
            union_regenerated_paths.update(
                _validate_edit_effect(
                    output_dir,
                    effect,
                    source_mapping_changed=source_mapping_changed,
                    numeric_present=numeric_present,
                )
            )
        else:
            expected_update = (
                "decision_manifest_only"
                if effect.get("target_artifact")
                else "review_record_only"
            )
            if effect.get("artifact_update") != expected_update:
                raise ValueError(
                    "Report Builder non-edit effect is not decision-derived."
                )
    numeric_pending = [
        _clean_text(section.get("section"))
        for section in analysis.get("sections", [])
        if isinstance(section, Mapping)
        and section.get("numeric_measure_status") == "needs_review"
    ]
    revision_paths = [
        f"revisions/report__{_safe_item_id(effect.get('item_id'))}.txt"
        for effect in edit_effects
    ]
    backup_paths = (
        [
            "revisions/originals/"
            f"report__{_safe_item_id(edit_effects[0].get('item_id'))}.docx"
        ]
        if edit_effects
        else []
    )
    review_history_paths = _validate_review_history(output_dir, applied)
    retained_review_paths = _validate_retained_review_paths(applied)
    predecessor_checkpoint = applied.get("predecessor_checkpoint")
    if review_history_paths:
        latest_history = _read_object(output_dir / review_history_paths[-1])
        if (
            not isinstance(predecessor_checkpoint, str)
            or re.fullmatch(r"[0-9a-f]{64}", predecessor_checkpoint) is None
            or latest_history.get("predecessor_checkpoint") != predecessor_checkpoint
        ):
            raise ValueError("Report Builder applied predecessor checkpoint is stale.")
    elif predecessor_checkpoint is not None:
        raise ValueError("Report Builder applied predecessor checkpoint is unexpected.")
    applied_item_count = (
        applied.get("item_count") if source_mapping_changed else len(items)
    )
    if not isinstance(applied_item_count, int) or applied_item_count < len(
        raw_decisions
    ):
        raise ValueError("Report Builder applied item count is malformed.")
    expected_values: dict[str, Any] = {
        "schema_version": review_payload.get("schema_version"),
        "plugin": review_payload.get("plugin"),
        "workflow": review_payload.get("workflow"),
        "run_id": review_payload.get("run_id"),
        "review_payload_sha256": _canonical_sha256(review_payload),
        "decision_count": len(raw_decisions),
        "item_count": applied_item_count,
        "blocker_count": sum(
            decision.get("action") in FOLLOWUP_ACTIONS for decision in raw_decisions
        ),
        "numeric_measure_pending_review_count": len(numeric_pending),
        "numeric_measure_pending_section_count": len(numeric_pending),
        "numeric_measure_pending_sections": numeric_pending,
        "revision_count": len(revision_paths),
        "revision_paths": revision_paths,
        "target_update_count": 0,
        "target_update_paths": [],
        "structured_update_count": 0,
        "structured_update_paths": [],
        "native_regeneration_count": 0,
        "native_regeneration_paths": [],
        "native_regenerated_count": len(edit_effects),
        "native_regenerated_paths": sorted(union_regenerated_paths),
        "original_backup_paths": backup_paths,
        "source_mapping_review_required": source_mapping_changed,
        "predecessor_checkpoint": predecessor_checkpoint,
        "review_history_paths": review_history_paths,
        "retained_review_paths": retained_review_paths,
    }
    if any(applied.get(key) != value for key, value in expected_values.items()):
        raise ValueError("Report Builder applied successor counters are stale.")
    review_binding = applied.get("review_payload")
    if not isinstance(review_binding, Mapping) or review_binding != {
        "path": "review_payload.json",
        "item_count": applied_item_count,
        "review_type": review_payload.get("review_type"),
    }:
        raise ValueError("Report Builder applied review binding is stale.")
    if not source_mapping_changed:
        if (
            applied.get("decision_source") != ui_decisions.get("decision_source")
            or applied.get("decision_review_payload_sha256")
            != _canonical_sha256(review_payload)
            or applied.get("reviewer") != ui_decisions.get("reviewer")
        ):
            raise ValueError("Report Builder applied decision authority is stale.")
    elif not re.fullmatch(
        r"[0-9a-f]{64}",
        _clean_text(applied.get("decision_review_payload_sha256")),
    ):
        raise ValueError("Report Builder predecessor decision digest is malformed.")
    expected_status = _expected_application_status(
        applied,
        analysis,
        source_mapping_changed=source_mapping_changed,
    )
    if applied.get("application_status") != expected_status:
        raise ValueError("Report Builder application status is not rederived.")
    review_application = final_artifacts.get("review_application")
    if not isinstance(review_application, Mapping):
        raise ValueError("Report Builder final successor state is missing.")
    final_expected = {
        "status": expected_status,
        "review_status": expected_status,
    }
    if any(final_artifacts.get(key) != value for key, value in final_expected.items()):
        raise ValueError("Report Builder final status is not successor-derived.")
    application_expected = {
        "applied_at": applied_at,
        "application_status": expected_status,
        "decision_count": len(raw_decisions),
        "item_count": applied_item_count,
        "blocker_count": expected_values["blocker_count"],
        "numeric_measure_pending_review_count": len(numeric_pending),
        "revision_count": len(revision_paths),
        "revision_paths": revision_paths,
        "target_update_count": 0,
        "target_update_paths": [],
        "structured_update_count": 0,
        "structured_update_paths": [],
        "native_regeneration_count": 0,
        "native_regeneration_paths": [],
        "native_regenerated_count": len(edit_effects),
        "native_regenerated_paths": sorted(union_regenerated_paths),
        "original_backup_paths": backup_paths,
        "numeric_measure_pending_section_count": len(numeric_pending),
        "numeric_measure_pending_sections": numeric_pending,
        "source_mapping_review_required": source_mapping_changed,
        "predecessor_checkpoint": predecessor_checkpoint,
        "review_history_paths": review_history_paths,
        "retained_review_paths": retained_review_paths,
        "applied_decisions_path": "applied_decisions.json",
    }
    if any(
        review_application.get(key) != value
        for key, value in application_expected.items()
    ):
        raise ValueError("Report Builder final application projection is stale.")
    return {
        "state": "applied",
        "decision_count": len(raw_decisions),
        "effect_count": len(raw_effects),
        "application_status": expected_status,
        "source_mapping_review_required": source_mapping_changed,
    }


def validate_review_successor(
    output_dir: Path,
    *,
    analysis: Mapping[str, Any],
    review_payload: Mapping[str, Any] | None = None,
    final_artifacts: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
    """Re-derive the current recorded-decision successor and its status."""

    root = Path(output_dir)
    review = (
        dict(review_payload)
        if isinstance(review_payload, Mapping)
        else _read_object(root / "review_payload.json")
    )
    final = (
        dict(final_artifacts)
        if isinstance(final_artifacts, Mapping)
        else _read_object(root / "final_artifacts.json")
    )
    ui = _read_object(root / "ui_decisions.json")
    ui_state = _validate_ui_state(review, ui)
    applied_path = root / "applied_decisions.json"
    if applied_path.is_file():
        state = _validate_applied_state(
            root,
            review,
            ui,
            _read_object(applied_path),
            analysis,
            final,
        )
    else:
        state = {
            "state": "reviewed" if ui_state else "pending",
            "decision_count": len(ui_state),
            "effect_count": 0,
            "application_status": None,
            "source_mapping_review_required": False,
        }
    return {
        "schema_version": SCHEMA_VERSION,
        **state,
        "reviewer_authentication": "not_established",
    }

SHA-256: 97617a6f695429d86c0163bf63bd426b8f786662eca74858458aee0605308135