fix: enforce SCADA chart device coverage
This commit is contained in:
@@ -77,6 +77,7 @@ from app.services.investigation_graph import adopt_legacy_investigation_graph, u
|
||||
from app.services.operational_presentation import resolve_overlay
|
||||
from app.services.operational_recovery import accept_completion, track_work_order
|
||||
from app.services.operational_scada_coverage import require_complete_scada_evidence
|
||||
from app.services import scada_presentation_coverage
|
||||
from app.services.time_comparison_chart import validate_time_comparison_chart
|
||||
|
||||
|
||||
@@ -955,6 +956,7 @@ class OperationalTaskService:
|
||||
require_complete_scada_evidence(
|
||||
task, await self.repo.list_evidence(str(task_id)), report_input.evidence_refs,
|
||||
)
|
||||
await self._verify_scada_report_presentation(task, active_run, _json(report_input), now)
|
||||
for gap in report_input.data_gaps:
|
||||
require(set(gap.evidence_refs) <= set(report_input.evidence_refs),
|
||||
"REPORT_GAP_EVIDENCE_INVALID",
|
||||
@@ -994,7 +996,7 @@ class OperationalTaskService:
|
||||
or previous_result.get("stage") != "final"
|
||||
or any(previous_result.get(key, [] if key == "ui_blocks" else None) != effective_report.get(key)
|
||||
for key in ("current_conclusion", "conclusion_boundary", "outcome", "stop_reason",
|
||||
"evidence_refs", "recommendations", "pending_items", "data_gaps", "ui_blocks"))
|
||||
"evidence_refs", "recommendations", "pending_items", "data_gaps", "ui_blocks", "scada_device_ids"))
|
||||
):
|
||||
await self._resolve_notifications(
|
||||
task_id, kinds=["report_updated"], subject_type="task", subject_id=task_id,
|
||||
@@ -1092,8 +1094,11 @@ class OperationalTaskService:
|
||||
now = self._now()
|
||||
self._require_active_run_lease(run, now)
|
||||
payload = _json(command.command)
|
||||
await self._record_scada_presentation(task, run, payload, now)
|
||||
if command.command.action == "open_chart":
|
||||
validate_time_comparison_chart(payload["chart"])
|
||||
validate_time_comparison_chart(payload["chart"], allow_missing_current=bool(
|
||||
payload["chart"].get("scada") and scada_presentation_coverage.applies(task),
|
||||
))
|
||||
known = {item["evidence_id"] for item in await self.repo.list_evidence(str(task_id))}
|
||||
self._require_evidence_refs(command.command.evidence_refs, known)
|
||||
elif command.command.action == "set_overlay":
|
||||
@@ -1131,6 +1136,61 @@ class OperationalTaskService:
|
||||
await self._save_request(scope, command.request_id, body, _json(result))
|
||||
return result
|
||||
|
||||
async def _record_scada_presentation(self, task: dict, run: dict, payload: dict, now: datetime) -> None:
|
||||
if not scada_presentation_coverage.applies(task):
|
||||
return
|
||||
action = payload["action"]
|
||||
targets = []
|
||||
if action == "render_scada_analysis":
|
||||
targets = [item["device_id"] for item in payload["items"]]
|
||||
elif action in {"locate_features", "highlight_features"} and payload["layer"] == "scada":
|
||||
targets = payload["feature_ids"]
|
||||
elif action != "open_chart":
|
||||
return
|
||||
evidence = await self.repo.list_evidence(task["task_id"])
|
||||
observations = scada_presentation_coverage.ScadaObservations(task, evidence)
|
||||
document = await self.repo.get_scada_presentation(task["task_id"], str(run["run_id"]))
|
||||
if action == "open_chart":
|
||||
if payload["chart"].get("scada"):
|
||||
known = {item["evidence_id"] for item in evidence}
|
||||
self._require_evidence_refs(payload["evidence_refs"], known)
|
||||
chart = scada_presentation_coverage.verify_chart(task, payload["chart"], evidence, payload["evidence_refs"])
|
||||
targets = chart["device_ids"]
|
||||
duplicates = [chart_id for chart_id, stored in (document or {}).get("charts", {}).items()
|
||||
if chart_id != payload["chart_id"] and stored["role"] == chart["role"] == "comparison"
|
||||
and any(set(stored.get("coverage", {}).get(device, {}).get("covered", []))
|
||||
& set(chart["coverage"][device]["covered"])
|
||||
for device in set(stored["device_ids"]) & set(targets))]
|
||||
require(not duplicates, "SCADA_CHART_DUPLICATE_DEVICE",
|
||||
f"设备同期图已存在,请更新原chart_id:{', '.join(duplicates)}", refs=duplicates)
|
||||
document = scada_presentation_coverage.add_targets(document, targets, observations)
|
||||
document["charts"][payload["chart_id"]] = chart
|
||||
elif document and payload["chart_id"] in document["charts"]:
|
||||
# Replacing a typed chart with a generic one removes its coverage,
|
||||
# while cumulative targets prevent shrinking the required set.
|
||||
del document["charts"][payload["chart_id"]]
|
||||
else:
|
||||
return
|
||||
else:
|
||||
document = scada_presentation_coverage.add_targets(document, targets, observations)
|
||||
await self.repo.save_scada_presentation(task["task_id"], str(run["run_id"]), document, now)
|
||||
|
||||
async def _verify_scada_report_presentation(self, task: dict, run: dict, report: dict, now: datetime) -> None:
|
||||
if not scada_presentation_coverage.applies(task):
|
||||
return
|
||||
if report.get("stage") != "final" and not report.get("scada_device_ids"):
|
||||
return
|
||||
evidence = await self.repo.list_evidence(task["task_id"])
|
||||
observations = scada_presentation_coverage.ScadaObservations(task, evidence)
|
||||
document = await self.repo.get_scada_presentation(task["task_id"], str(run["run_id"]))
|
||||
targets = list(report.get("scada_device_ids") or [])
|
||||
if targets:
|
||||
document = scada_presentation_coverage.add_targets(document, targets, observations)
|
||||
if report.get("stage") == "final":
|
||||
scada_presentation_coverage.require_complete(task, document, observations)
|
||||
if document:
|
||||
await self.repo.save_scada_presentation(task["task_id"], str(run["run_id"]), document, now)
|
||||
|
||||
async def propose_action(
|
||||
self, task_id: UUID, command: ActionProposalCommand
|
||||
) -> HumanApprovalAction:
|
||||
@@ -2049,6 +2109,7 @@ class OperationalTaskService:
|
||||
"Run requires a final report from the same generation"
|
||||
)
|
||||
else:
|
||||
await self._verify_scada_report_presentation(task, run, _json(report.report), now)
|
||||
actions = [
|
||||
HumanApprovalAction.model_validate(item)
|
||||
for item in await self.repo.list_actions(str(task_id))
|
||||
|
||||
Reference in New Issue
Block a user