feat(investigations): add event impact and work-order feedback flows

Add isolated burst and pollution impact evidence, parallel work-order workflows, persisted notifications and graph updates.

Checkpoint current code. Known follow-ups remain for large pollution result previews and graceful geometry failure.
This commit is contained in:
2026-11-14 15:14:02 +08:00
parent 471ad92a47
commit 2e4f6cb93b
49 changed files with 1826 additions and 93 deletions
+34
View File
@@ -0,0 +1,34 @@
"""Temporary deterministic candidate selection behind the source-tracing contract.
Replace this selection with source inversion when the algorithm is available.
Only project-owned nodes supplied from the investigation are eligible. No
observation fit, confidence or contamination extent is inferred here.
"""
from psycopg import AsyncConnection
from app.infra.db.postgresql.read_queries import read_rows
from app.mcp.analysis_models import ContaminantSourceTraceInput
async def trace_contaminant_source(connection: AsyncConnection, args: ContaminantSourceTraceInput) -> dict:
rows = await read_rows(connection, """
SELECT id, node_type::text AS kind FROM network.nodes
WHERE id = ANY(%s) ORDER BY id
""", (args.candidate_node_ids,))
# Sort in Python as well so selection does not depend on DB collation.
nodes = sorted({str(row["id"]) for row in rows})
selected = nodes[0] if nodes else None
return {
"located_node": selected,
"top_candidates": [{"node_id": selected}] if selected else [],
"exit_condition": "candidate_selected" if selected else "no_candidate",
"method": "deterministic_candidate_selection",
"algorithm_status": "provisional",
"selection_basis": "lexicographically_first_existing_candidate_node",
"validated_node_ids": nodes,
"missing_node_ids": sorted(set(args.candidate_node_ids) - set(nodes)),
"notice": ("已返回待现场核验的候选节点;先展示节点、保存阶段结论并提出现场核查/采样处置工单,"
"工单审核派发与污染传播模拟、节点隔离及影响分析并行,结果补入同一调查。"
"正式污染反演算法尚未接入,本次选择不代表污染源确诊或污染传播范围。" if selected else
"提交的候选节点均不在当前项目模型中;核对事件位置及资产映射后重新提交,继续不依赖源节点的调查。"),
}