feat: redesign persisted session summaries

This commit is contained in:
2026-07-23 15:35:31 +02:00
parent 12c7a976d7
commit 0db85e4e4e
12 changed files with 1090 additions and 71 deletions
+166 -33
View File
@@ -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))