feat(investigation): preserve cumulative reference graphs

Accumulate investigation paths and preserve failed attempts across rounds. Keep graph annotations separate from execution state, synchronize the API contract, and consolidate documentation.
This commit is contained in:
2026-11-11 19:10:41 +08:00
parent e47c61c121
commit b853b61a0b
38 changed files with 2445 additions and 147 deletions
@@ -628,6 +628,37 @@ class OperationalTaskRepository:
row = await cursor.fetchone()
return row["document"] if row else None
async def get_latest_graph_progress(self, task_id: str) -> dict | None:
availability = await self.connection.execute(
"SELECT to_regclass('workbench.operational_task_progress') AS relation"
)
if not (await availability.fetchone())["relation"]:
return None
cursor = await self.connection.execute(
"""SELECT document FROM workbench.operational_task_progress
WHERE project_id=%s AND task_id=%s
AND jsonb_typeof(document#>'{progress,diagram,edges}')='array'
ORDER BY (document->>'execution_generation')::bigint DESC, revision DESC LIMIT 1""",
(self.project_id, task_id),
)
row = await cursor.fetchone()
return row["document"] if row else None
async def get_latest_diagram(self, task_id: str) -> dict | None:
cursor = await self.connection.execute(
"""SELECT diagram FROM (
SELECT document#>'{progress,diagram}' AS diagram, published_at AS occurred_at
FROM workbench.operational_task_progress WHERE project_id=%s AND task_id=%s
UNION ALL
SELECT document#>'{report,diagram}', created_at
FROM workbench.operational_task_reports WHERE project_id=%s AND task_id=%s
) AS diagrams WHERE jsonb_typeof(diagram)='object'
ORDER BY occurred_at DESC LIMIT 1""",
(self.project_id, task_id, self.project_id, task_id),
)
row = await cursor.fetchone()
return row["diagram"] if row else None
async def save_progress(self, task_id: str, snapshot: dict) -> None:
cursor = await self.connection.execute(
"""INSERT INTO workbench.operational_task_progress
+32 -2
View File
@@ -7,7 +7,9 @@ from uuid import UUID
from psycopg import AsyncConnection
from psycopg import sql
from psycopg.rows import dict_row
from psycopg.types.json import Jsonb
from fastapi import HTTPException
from app.domain.schemas.operational_task import LEGACY_INVESTIGATION_NODE_ANNOTATIONS
from app.infra.db.postgresql.scada import _SCADA_VIEW_SELECT, _device
from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository
from app.infra.db.timescaledb.repositories.analysis import AnalysisResultsRepository
@@ -329,6 +331,29 @@ async def task_context_summary(conn: AsyncConnection, *, project_id: UUID, task_
return result
def _reference_diagram_projection(stored_sql: str, stored_params: tuple, section: str) -> tuple[str, tuple]:
"""Normalize only saved diagram annotations before any data_path or page projection."""
if section not in {"progress", "report"}:
raise ValueError("reference diagrams only belong to progress or report sections")
path = "{data," + section + ",diagram,node_refs}"
statement = f"""
WITH reference_documents AS (
{stored_sql}
), reference_aliases AS (
SELECT %s::jsonb AS annotations
)
SELECT CASE WHEN jsonb_typeof(snapshot#>'{path}')='array' THEN
jsonb_set(snapshot,'{path}',COALESCE((
SELECT jsonb_agg(CASE WHEN annotations ? (node->>'status')
THEN jsonb_set(node,'{{status}}',annotations->(node->>'status'))
ELSE node END ORDER BY position)
FROM jsonb_array_elements(snapshot#>'{path}') WITH ORDINALITY AS nodes(node,position)
),'[]'::jsonb)) ELSE snapshot END AS snapshot
FROM reference_documents CROSS JOIN reference_aliases
"""
return statement, (*stored_params, Jsonb(LEGACY_INVESTIGATION_NODE_ANNOTATIONS))
async def task_context_page(
conn: AsyncConnection, *, project_id: UUID, task_id: UUID, section: str,
report_revision: int | None, data_path: list[str], limit: int, offset: int,
@@ -386,11 +411,16 @@ async def task_context_page(
stored = """SELECT jsonb_build_object('section','progress','data',p.document) AS snapshot
FROM workbench.operational_task_progress p
JOIN workbench.operational_task_runs r ON r.project_id=p.project_id AND r.task_id=p.task_id AND r.run_id=p.run_id
JOIN workbench.operational_tasks t ON t.project_id=p.project_id AND t.task_id=p.task_id AND t.execution_generation=r.generation
WHERE p.project_id=%s AND p.task_id=%s"""
JOIN workbench.operational_tasks t ON t.project_id=p.project_id AND t.task_id=p.task_id
WHERE p.project_id=%s AND p.task_id=%s AND r.generation<=t.execution_generation
AND (t.execution_generation=r.generation
OR jsonb_typeof(p.document#>'{progress,diagram,edges}')='array')
ORDER BY (t.execution_generation=r.generation) DESC,r.generation DESC,p.revision DESC LIMIT 1"""
params = (project_id, task_id)
else:
raise HTTPException(422, "Unknown task context section")
if section in {"progress", "report"}:
stored, params = _reference_diagram_projection(stored, params, section)
return await _snapshot_page(conn, stored_sql=stored, stored_params=params,
data_path=data_path, limit=limit, offset=projection_offset, mcp_sources=[], max_bytes=8000)