feat: redesign persisted session summaries
This commit is contained in:
+166
-33
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
@@ -143,22 +144,160 @@ def new_session_manifest(
|
||||
)
|
||||
|
||||
|
||||
def build_snapshot_documents(snapshot) -> list[dict]:
|
||||
docs = [{"phase": "—", "key": "question", "title": "Original question", "format": "text", "content": snapshot.manifest.question}]
|
||||
spec = [
|
||||
("question", "F3", "revised_question", "Revised question", "markdown"),
|
||||
("schema_linking", "F4", "schema_linking", "Schema linking", "schema-linking"),
|
||||
("sql_final", "F7", "sql", "Final SQL", "sql"),
|
||||
("validation_report", "finalize", "validation_report", "Validation report", "markdown"),
|
||||
_ASSUMPTIONS_HEADING = re.compile(
|
||||
r"^#{1,6}\s+(?:assunzioni|assumptions)\s*$", re.IGNORECASE | re.MULTILINE
|
||||
)
|
||||
_PREVIEW_HEADING = re.compile(
|
||||
r"^##\s+preview(?:\s*/\s*aggregato)?\s*$", re.IGNORECASE | re.MULTILINE
|
||||
)
|
||||
_NEXT_H2 = re.compile(r"^##\s+", re.MULTILINE)
|
||||
_MEMORY_TYPES = frozenset(
|
||||
{"concept_clarified", "memory_promoted", "memory_promotion_declined", "memory_rejected"}
|
||||
)
|
||||
_SUMMARY_HIDDEN_TYPES = _MEMORY_TYPES | frozenset(
|
||||
{
|
||||
"phase_approved",
|
||||
"phase_auto_approved",
|
||||
"table_approved",
|
||||
"table_promoted",
|
||||
"column_promoted",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _split_question_sections(content: str) -> tuple[str, str]:
|
||||
"""Split legacy combined question Markdown without rewriting the artifact."""
|
||||
match = _ASSUMPTIONS_HEADING.search(content)
|
||||
if match is None:
|
||||
return content.strip(), ""
|
||||
return content[:match.start()].strip(), content[match.start():].strip()
|
||||
|
||||
|
||||
def _split_validation_preview(content: str) -> tuple[str, str]:
|
||||
"""Return the persisted preview section and the remaining validation report."""
|
||||
match = _PREVIEW_HEADING.search(content)
|
||||
if match is None:
|
||||
return "", content.strip()
|
||||
body_start = match.end()
|
||||
next_heading = _NEXT_H2.search(content, body_start)
|
||||
section_end = next_heading.start() if next_heading else len(content)
|
||||
preview = content[body_start:section_end].strip()
|
||||
remaining = (content[:match.start()] + content[section_end:]).strip()
|
||||
return preview, remaining
|
||||
|
||||
|
||||
def _referenced_decision(marker, by_seq: dict[int, object]):
|
||||
match = re.fullmatch(r"seq:(\d+)", marker.detail.strip())
|
||||
return by_seq.get(int(match.group(1))) if match else None
|
||||
|
||||
|
||||
def _memory_projection(decisions: list) -> list[dict[str, str]]:
|
||||
by_seq = {decision.seq: decision for decision in decisions}
|
||||
approved = [decision for decision in decisions if decision.type == "memory_promoted"]
|
||||
declined = [
|
||||
decision
|
||||
for decision in decisions
|
||||
if decision.type in {"memory_promotion_declined", "memory_rejected"}
|
||||
]
|
||||
for artifact, phase, key, title, fmt in spec:
|
||||
if artifact in snapshot.artifacts:
|
||||
docs.append({"phase": phase, "key": key, "title": title, "format": fmt, "content": snapshot.artifacts[artifact]})
|
||||
if snapshot.decisions:
|
||||
docs.append({"phase": "—", "key": "decisions", "title": "Decisions", "format": "decisions", "content": "\n".join(d.model_dump_json() for d in snapshot.decisions) + "\n"})
|
||||
items: list[dict[str, str]] = []
|
||||
for status, markers in (("approved", approved), ("declined", declined)):
|
||||
for marker in markers:
|
||||
origin = _referenced_decision(marker, by_seq)
|
||||
detail = origin.detail if origin is not None else marker.detail
|
||||
if re.fullmatch(r"seq:\d+", detail.strip()):
|
||||
detail = ""
|
||||
rationale = (
|
||||
origin.rationale if origin is not None and origin.rationale
|
||||
else marker.rationale
|
||||
)
|
||||
if rationale.strip() == detail.strip():
|
||||
rationale = ""
|
||||
items.append(
|
||||
{
|
||||
"status": status,
|
||||
"subject": (origin.subject if origin is not None else marker.subject),
|
||||
"detail": detail,
|
||||
"rationale": rationale,
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
def _document_bundle(manifest: SessionManifest, artifacts: dict[str, str], decisions: list) -> list[dict]:
|
||||
docs: list[dict] = [{
|
||||
"phase": "—",
|
||||
"key": "question",
|
||||
"title": "Original question",
|
||||
"format": "text",
|
||||
"content": manifest.question,
|
||||
}]
|
||||
|
||||
sql = artifacts.get("sql_final")
|
||||
if sql is not None:
|
||||
docs.append({
|
||||
"phase": "F7", "key": "sql", "title": "Final SQL",
|
||||
"format": "sql", "content": sql,
|
||||
})
|
||||
|
||||
validation = artifacts.get("validation_report")
|
||||
preview, validation_without_preview = _split_validation_preview(validation or "")
|
||||
if preview:
|
||||
docs.append({
|
||||
"phase": "finalize", "key": "preview", "title": "Data preview",
|
||||
"format": "markdown", "content": preview,
|
||||
})
|
||||
|
||||
revised, assumptions = _split_question_sections(artifacts.get("question", ""))
|
||||
if revised:
|
||||
docs.append({
|
||||
"phase": "F3", "key": "revised_question", "title": "Revised question",
|
||||
"format": "markdown", "content": revised,
|
||||
})
|
||||
if assumptions:
|
||||
docs.append({
|
||||
"phase": "F3", "key": "assumptions", "title": "Assumptions",
|
||||
"format": "markdown", "content": assumptions,
|
||||
})
|
||||
|
||||
memories = _memory_projection(decisions)
|
||||
if memories:
|
||||
docs.append({
|
||||
"phase": "F8", "key": "memories", "title": "Memories",
|
||||
"format": "memories", "content": json.dumps(memories, ensure_ascii=False),
|
||||
})
|
||||
|
||||
schema_linking = artifacts.get("schema_linking")
|
||||
if schema_linking is not None:
|
||||
docs.append({
|
||||
"phase": "F4", "key": "schema_linking", "title": "Schema linking",
|
||||
"format": "schema-linking", "content": schema_linking,
|
||||
})
|
||||
if validation_without_preview:
|
||||
docs.append({
|
||||
"phase": "finalize", "key": "validation_report", "title": "Validation report",
|
||||
"format": "markdown", "content": validation_without_preview,
|
||||
})
|
||||
|
||||
visible_decisions = [d for d in decisions if d.type not in _SUMMARY_HIDDEN_TYPES]
|
||||
if visible_decisions:
|
||||
docs.append({
|
||||
"phase": "—", "key": "decisions", "title": "Decisions",
|
||||
"format": "decisions",
|
||||
"content": "\n".join(d.model_dump_json() for d in visible_decisions) + "\n",
|
||||
})
|
||||
return docs
|
||||
|
||||
|
||||
def build_snapshot_documents(snapshot) -> list[dict]:
|
||||
from tht.phase import effective_decisions
|
||||
|
||||
return _document_bundle(
|
||||
snapshot.manifest,
|
||||
snapshot.artifacts,
|
||||
effective_decisions(snapshot),
|
||||
)
|
||||
|
||||
|
||||
def touch_manifest(
|
||||
session_id: str, sessions_root: Path, *, updated_by: str | None = None
|
||||
) -> SessionManifest:
|
||||
@@ -379,24 +518,18 @@ def persist_verified_finalization(
|
||||
|
||||
|
||||
def build_documents(manifest: SessionManifest, session_dir: Path) -> list[dict]:
|
||||
"""Ordered, read-only document bundle for the UI panel. Only documents that exist
|
||||
on disk are returned. CTE artifacts (F6) are intentionally excluded (intermediate)."""
|
||||
docs: list[dict] = [{
|
||||
"phase": "—", "key": "question", "title": "Original question",
|
||||
"format": "text", "content": manifest.question,
|
||||
}]
|
||||
spec = [
|
||||
("question.md", "F3", "revised_question", "Revised question", "markdown"),
|
||||
("schema_linking.json", "F4", "schema_linking", "Schema linking", "schema-linking"),
|
||||
("sql_final.sql", "F7", "sql", "Final SQL", "sql"),
|
||||
("validation_report.md", "finalize", "validation_report", "Validation report", "markdown"),
|
||||
("review_decisions.jsonl", "—", "decisions", "Decisions", "decisions"),
|
||||
]
|
||||
for filename, phase, key, title, fmt in spec:
|
||||
path = session_dir / filename
|
||||
if path.exists():
|
||||
docs.append({
|
||||
"phase": phase, "key": key, "title": title,
|
||||
"format": fmt, "content": path.read_text(),
|
||||
})
|
||||
return docs
|
||||
"""Build the canonical read-only bundle for legacy filesystem sessions."""
|
||||
from tht.phase import effective_decisions
|
||||
|
||||
artifact_files = {
|
||||
"question": "question.md",
|
||||
"schema_linking": "schema_linking.json",
|
||||
"sql_final": "sql_final.sql",
|
||||
"validation_report": "validation_report.md",
|
||||
}
|
||||
artifacts = {
|
||||
key: path.read_text()
|
||||
for key, filename in artifact_files.items()
|
||||
if (path := session_dir / filename).exists()
|
||||
}
|
||||
return _document_bundle(manifest, artifacts, effective_decisions(session_dir))
|
||||
|
||||
Reference in New Issue
Block a user