| from __future__ import annotations |
|
|
| import html |
| import itertools |
| from typing import Iterable |
|
|
| from .models import EvidenceItem |
|
|
| _counter = itertools.count(1) |
|
|
|
|
| def reset_evidence_counter() -> None: |
| global _counter |
| _counter = itertools.count(1) |
|
|
|
|
| def make_evidence( |
| *, |
| category: str, |
| finding: str, |
| source_name: str, |
| source_url: str, |
| source_type: str, |
| resolution_or_scope: str, |
| confidence: str, |
| limitation: str, |
| design_implication: str, |
| verification_needed: str, |
| output_label: str, |
| ) -> EvidenceItem: |
| if confidence not in {"high", "medium", "low"}: |
| raise ValueError(f"Invalid confidence: {confidence}") |
| if output_label not in { |
| "computed", |
| "public_data", |
| "cad_derived", |
| "user_input", |
| "ai_interpretation", |
| "site_visit_required", |
| "professional_verification_required", |
| }: |
| raise ValueError(f"Invalid output label: {output_label}") |
| return EvidenceItem( |
| id=f"E{next(_counter):03d}", |
| category=category, |
| finding=finding, |
| source_name=source_name, |
| source_url=source_url, |
| source_type=source_type, |
| resolution_or_scope=resolution_or_scope, |
| confidence=confidence, |
| limitation=limitation, |
| design_implication=design_implication, |
| verification_needed=verification_needed, |
| output_label=output_label, |
| ) |
|
|
|
|
| def evidence_to_markdown_table(items: Iterable[EvidenceItem]) -> str: |
| rows = list(items) |
| if not rows: |
| return "_No evidence rows were generated._" |
| header = ( |
| "| ID | Category | Finding | Source | Confidence | Limitation | " |
| "Design implication | Verify on site |\n" |
| "|---|---|---|---|---|---|---|---|" |
| ) |
| body = [] |
| for item in rows: |
| source = _md(item.source_name) |
| if item.source_url: |
| source = f"[{source}]({item.source_url})" |
| body.append( |
| "| {id} | {category} | {finding} | {source} | {confidence} | " |
| "{limitation} | {implication} | {verify} |".format( |
| id=_md(item.id), |
| category=_md(item.category), |
| finding=_md(item.finding), |
| source=source, |
| confidence=_md(item.confidence), |
| limitation=_md(item.limitation), |
| implication=_md(item.design_implication), |
| verify=_md(item.verification_needed), |
| ) |
| ) |
| return header + "\n" + "\n".join(body) |
|
|
|
|
| def evidence_to_dataframe_rows(items: Iterable[EvidenceItem]) -> list[dict[str, str]]: |
| return [ |
| { |
| "ID": item.id, |
| "Category": item.category, |
| "Finding": item.finding, |
| "Source": item.source_name, |
| "Source URL": item.source_url, |
| "Type": item.source_type, |
| "Scope / resolution": item.resolution_or_scope, |
| "Confidence": item.confidence, |
| "Limitation": item.limitation, |
| "Design implication": item.design_implication, |
| "Verify": item.verification_needed, |
| "Label": item.output_label, |
| } |
| for item in items |
| ] |
|
|
|
|
| def evidence_to_html_table(items: Iterable[EvidenceItem]) -> str: |
| rows = list(items) |
| if not rows: |
| return "<div class='evidence-shell'><p>No evidence rows were generated.</p></div>" |
| body = [] |
| for item in rows: |
| source = html.escape(item.source_name or "Not available") |
| if item.source_url: |
| safe_url = html.escape(item.source_url, quote=True) |
| source = f"<a href='{safe_url}' target='_blank' rel='noopener noreferrer'>{source}</a>" |
| body.append( |
| "<tr>" |
| f"<td class='evidence-id'>{html.escape(item.id)}</td>" |
| f"<td>{html.escape(item.category)}</td>" |
| f"<td>{html.escape(item.finding)}</td>" |
| f"<td>{source}<small>{html.escape(item.source_type)}</small></td>" |
| f"<td><span class='confidence confidence-{html.escape(item.confidence)}'>{html.escape(item.confidence)}</span></td>" |
| f"<td>{html.escape(item.resolution_or_scope)}</td>" |
| f"<td>{html.escape(item.limitation)}</td>" |
| f"<td>{html.escape(item.design_implication)}</td>" |
| f"<td>{html.escape(item.verification_needed)}</td>" |
| "</tr>" |
| ) |
| return ( |
| "<div class='evidence-shell'>" |
| "<div class='evidence-header'>" |
| "<div><span>Evidence Audit</span><strong>Every board claim must trace to a source, calculation, or verification rule.</strong></div>" |
| f"<b>{len(rows)} rows</b>" |
| "</div>" |
| "<div class='evidence-scroll'>" |
| "<table class='evidence-html-table'>" |
| "<thead><tr>" |
| "<th>ID</th><th>Category</th><th>Finding</th><th>Source</th><th>Confidence</th>" |
| "<th>Scope</th><th>Limitation</th><th>Design implication</th><th>Verify</th>" |
| "</tr></thead>" |
| "<tbody>" |
| + "".join(body) |
| + "</tbody></table></div></div>" |
| ) |
|
|
|
|
| def _md(text: object) -> str: |
| escaped = html.escape("" if text is None else str(text)) |
| return escaped.replace("|", "\\|").replace("\n", " ") |
|
|