from __future__ import annotations import re from typing import Any SECTION_RULES = [ (("pond", "storage", "freeboard", "spillway", "forebay"), "Storage and Pond Design"), (("pipe", "conduit", "sewer", "hgl", "surcharge", "velocity"), "Minor-System Hydraulics"), (("overland", "major system", "spill route", "surface drainage"), "Major-System Assessment"), (("catchment", "subcatchment", "impervious", "runoff", "hydrology"), "Hydrology and Catchment Parameters"), (("rainfall", "storm", "idf", "hyetograph"), "Design Storm"), (("water quality", "tss", "erosion", "sediment"), "Water Quality"), (("drawing", "profile", "plan", "detail"), "Drawing Coordination"), (("model", "swmm", "input file", "output file"), "Computer Model and Appendices"), ] def parse_comments(value: list | str) -> list[dict[str, Any]]: if isinstance(value, list): rows = [] for n, item in enumerate(value, 1): if isinstance(item, dict): text = str(item.get("comment") or item.get("text") or "").strip() cid = str(item.get("comment_id") or item.get("id") or n) else: text = str(item).strip(); cid = str(n) if text: rows.append({"comment_id": cid, "comment": text}) return rows if not isinstance(value, str): raise ValueError("comments must be a list or text string.") text = value.strip() if not text: return [] parts = re.split(r"(?m)^\s*(?:Comment\s*)?(\d+[A-Za-z]?)\s*[.):-]\s*", text) rows = [] if len(parts) > 1: prefix = parts[0].strip() for i in range(1, len(parts), 2): cid = parts[i].strip(); body = parts[i+1].strip() if i+1 < len(parts) else "" if body: rows.append({"comment_id": cid, "comment": body}) if rows: return rows return [{"comment_id": str(n), "comment": line.strip(" - ")} for n, line in enumerate(text.splitlines(), 1) if line.strip()] def _section(comment: str) -> str: lower = comment.lower() for words, section in SECTION_RULES: if any(word in lower for word in words): return section return "General / Executive Summary" def build_response_matrix(comments: list[dict[str, Any]], revision_review: dict[str, Any] | None = None, report_changes: dict[str, str] | None = None) -> list[dict[str, Any]]: revision_rows = (revision_review or {}).get("object_impacts", []) report_changes = report_changes or {} result = [] for item in comments: text = item["comment"] mentioned = [] for row in revision_rows: oid = str(row.get("object", "")) if oid and re.search(rf"\b{re.escape(oid)}\b", text, re.I): mentioned.append(row) evidence = [] for row in mentioned: evidence.append(f"{row.get('object')}: revision impact = {row.get('impact')}") for ev in row.get("evidence", [])[:3]: if isinstance(ev, dict): evidence.append( f"{ev.get('metric')}: {ev.get('baseline')} -> {ev.get('revised')} {ev.get('units') or ''}".strip() ) section = _section(text) changed = report_changes.get(str(item["comment_id"])) or report_changes.get(section) if evidence and changed: status = "Resolved" elif evidence or changed: status = "Partially resolved" else: status = "Clarification required" response = ( "The comment has been mapped to deterministic model/revision evidence and the revised report section." if status == "Resolved" else "Additional project-specific evidence or an explicit report/drawing revision is required before this comment can be closed." ) result.append({ "comment_id": item["comment_id"], "city_comment": text, "topic": section, "affected_objects": ", ".join(str(r.get("object")) for r in mentioned), "engineering_evidence": "; ".join(evidence), "report_change": changed or "Not supplied", "consultant_response": response, "status": status, }) return result