lct-55: ошибка адреса и сообщение в 112

This commit is contained in:
kaifarikman 2026-09-28 00:55:28 +03:00 • committed by gglamer
commit f0c9015f2b
14 changed files with 267 additions and 7 deletions

View file

@ -678,6 +678,22 @@ class CardReply(BaseModel):
text: str = Field(max_length=2000) text: str = Field(max_length=2000)
class CardErrorReport112(BaseModel):
"""Сообщение диспетчера в 112 после доклада бригады с места."""
type: Literal["card.error_report_112"] = "card.error_report_112"
card_id: UUID
text: str = Field(min_length=10, max_length=500)
@field_validator("text")
@classmethod
def meaningful_text(cls, value: str) -> str:
value = value.strip()
if len(value) < 10:
raise ValueError("Укажите, что сообщено оператору 112")
return value
class CardNext(BaseModel): class CardNext(BaseModel):
type: Literal["card.next"] = "card.next" type: Literal["card.next"] = "card.next"
card_id: UUID card_id: UUID
@ -721,7 +737,7 @@ ServerToStation = Annotated[
StationToServer = Annotated[ StationToServer = Annotated[
CardAck | CardBounce | ServiceStatusSet | CrewSelect | PhoneDial | PhoneBrief | PhoneCheck CardAck | CardBounce | ServiceStatusSet | CrewSelect | PhoneDial | PhoneBrief | PhoneCheck
| PhoneHangup | PhoneReply | AdjacentDial | StationFinish | PhoneHangup | PhoneReply | AdjacentDial | StationFinish
| CardReply | CardNext | CardOpen | CardReply | CardErrorReport112 | CardNext | CardOpen
| ZoneDecision | CrewDispatched | CrewArrived, | ZoneDecision | CrewDispatched | CrewArrived,
Field(discriminator="type"), Field(discriminator="type"),
] ]

View file

@ -288,6 +288,8 @@ class StationSnapshot(BaseModel):
phone_reports: list[PhoneReportRecord] = [] phone_reports: list[PhoneReportRecord] = []
phone_lines: list[PhoneLineRecord] = [] phone_lines: list[PhoneLineRecord] = []
phone_pending: PhoneCallPending | None = None phone_pending: PhoneCallPending | None = None
card_error_discovered: bool = False
report_112_text: str | None = None
card_id: UUID | None = None card_id: UUID | None = None
card_index: int = 1 card_index: int = 1
card_total: int = 1 card_total: int = 1

View file

@ -5,12 +5,15 @@
на старте приложения, а не тихо игнорироваться и всплывать посреди занятия. на старте приложения, а не тихо игнорироваться и всплывать посреди занятия.
""" """
from pydantic import BaseModel, ConfigDict, Field, model_validator from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.domain.classifiers import DDSCode, IncidentType, Level, Outcome from app.domain.classifiers import DDSCode, IncidentType, Level, Outcome
from app.domain.events import Mood from app.domain.events import Mood
from app.domain.kio import EDITABLE_KIO_FIELDS, KIO from app.domain.kio import EDITABLE_KIO_FIELDS, KIO
from app.domain.statuses import NEXT, SERVICE_STATUS_LABELS, ServiceStatus from app.domain.statuses import NEXT, SERVICE_STATUS_LABELS, ServiceStatus
from app.scoring.address import address_matches
from app.scoring.taxonomy import METRIC_MAP from app.scoring.taxonomy import METRIC_MAP
@ -174,6 +177,21 @@ FALLBACK_NOTIFY: dict[str, str] = {
} }
class DdsCardError(Strict):
"""Заложенное расхождение готовой карточки с эталоном для занятия ДДС."""
field: Literal["address"]
card_value: str = Field(min_length=3)
@field_validator("card_value")
@classmethod
def nonempty_card_value(cls, value: str) -> str:
value = value.strip()
if len(value) < 3:
raise ValueError("dds_card_error.card_value: укажите неверный адрес карточки")
return value
class Scenario(Strict): class Scenario(Strict):
id: str id: str
title: str title: str
@ -202,6 +220,7 @@ class Scenario(Strict):
# справка и передача в другой регион разбираются в lct-36. # справка и передача в другой регион разбираются в lct-36.
outcome: Outcome = Outcome.CARD outcome: Outcome = Outcome.CARD
dds_decision: DdsDecision = DdsDecision() dds_decision: DdsDecision = DdsDecision()
dds_card_error: DdsCardError | None = None
# Готовая КИО, сформированная курсантом и утверждённая преподавателем. # Готовая КИО, сформированная курсантом и утверждённая преподавателем.
# Для системных сценариев поле отсутствует; ДДС использует снимок как # Для системных сценариев поле отсутствует; ДДС использует снимок как
@ -258,6 +277,19 @@ class Scenario(Strict):
) )
return self return self
@model_validator(mode="after")
def valid_dds_card_error(self):
error = self.dds_card_error
if error is None:
return self
if not self.ground_truth.address or not self.ground_truth.address.strip():
raise ValueError("dds_card_error: для ошибки адреса нужен ground_truth.address")
if self.student_card is not None:
raise ValueError("dds_card_error: используйте отдельный учебный сценарий без student_card")
if address_matches(self.ground_truth.address, error.card_value):
raise ValueError("dds_card_error: адрес карточки должен отличаться от эталона")
return self
@model_validator(mode="after") @model_validator(mode="after")
def unique_reference_ids(self): def unique_reference_ids(self):
for label, values in ( for label, values in (

View file

@ -99,6 +99,11 @@ def to_csv(report: SessionReport) -> bytes:
row("Сверка самооценки", "", label, ", ".join(values)) row("Сверка самооценки", "", label, ", ".join(values))
for number, item in enumerate(report.notes, 1): for number, item in enumerate(report.notes, 1):
row("Пометки преподавателя", number, item.transcript_ref, item.text, item.author) row("Пометки преподавателя", number, item.transcript_ref, item.text, item.author)
for card_number, card in enumerate(report.card_results, 1):
for action in card.actions:
if action.get("type") == "card.error_report_112":
row("Сообщение в 112", card_number, "Ошибка карточки",
action.get("text", ""), action.get("at", ""))
return ("\N{ZERO WIDTH NO-BREAK SPACE}" + output.getvalue()).encode("utf-8") return ("\N{ZERO WIDTH NO-BREAK SPACE}" + output.getvalue()).encode("utf-8")
@ -259,6 +264,17 @@ def to_pdf(report: SessionReport) -> bytes:
for item in report.notes: for item in report.notes:
story.append(p(f"{item.transcript_ref} - {item.author}: {item.text}")) story.append(p(f"{item.transcript_ref} - {item.author}: {item.text}"))
messages_112 = [
(number, action)
for number, card in enumerate(report.card_results, 1)
for action in card.actions
if action.get("type") == "card.error_report_112"
]
if messages_112:
section("Сообщения в 112 об ошибках карточек")
for number, action in messages_112:
story.append(p(f"Карточка {number}, {action.get('at', '')}: {action.get('text', '')}"))
output = io.BytesIO() output = io.BytesIO()
document = SimpleDocTemplate( document = SimpleDocTemplate(
output, pagesize=A4, leftMargin=42, rightMargin=42, output, pagesize=A4, leftMargin=42, rightMargin=42,

View file

@ -195,6 +195,9 @@ def build_card(
) )
if service: if service:
kio = apply_patch(kio, {"dds": service}) kio = apply_patch(kio, {"dds": service})
if scenario.dds_card_error is not None:
# Ошибка относится к выданному снимку; эталон сценария остаётся верным.
kio = kio.model_copy(update={"address": scenario.dds_card_error.card_value})
kio.response_status = ResponseStatus.TRANSFERRED kio.response_status = ResponseStatus.TRANSFERRED
timers = SessionTimers(limits=dict(limits)) timers = SessionTimers(limits=dict(limits))
timers.on_event("dds.dispatch") timers.on_event("dds.dispatch")
@ -401,6 +404,13 @@ def _deliver_card_adjacent(state: "SessionState", card: DdsLiveCard, now: dateti
def _finish_phone_call(card: DdsLiveCard) -> list[Any]: def _finish_phone_call(card: DdsLiveCard) -> list[Any]:
call = card.phone_pending call = card.phone_pending
assert call is not None assert call is not None
error = card.scenario.dds_card_error
if call.phase == "arrived" and error is not None and error.field == "address":
actual = card.scenario.ground_truth.address
text = (f"{call.crew}: прибыли по адресу {actual}. "
f"В карточке указан {card.dispatched_card.address}; "
"адрес не совпадает. Сообщите об ошибке в 112.")
else:
text = _report_text(call.phase, call.crew, card.dispatched_card.address or "из карточки") text = _report_text(call.phase, call.crew, card.dispatched_card.address or "из карточки")
line = _line(card, "crew", text) line = _line(card, "crew", text)
report = PhoneReportRecord(service=call.service, crew=call.crew, report = PhoneReportRecord(service=call.service, crew=call.crew,
@ -749,6 +759,16 @@ class DdsDesk(BaseModel):
return DeskOutcome() return DeskOutcome()
card.reply_text = command.text card.reply_text = command.text
card.reply_log.append((now_utc(), command.text)) card.reply_log.append((now_utc(), command.text))
case "card.error_report_112":
if command.card_id != card.card_id:
return _refused("Сообщение относится не к текущей карточке")
if card.scenario.dds_card_error is None or not any(
report.phase == "arrived" for report in card.phone_reports
):
return _refused("Сначала получите доклад бригады с места об ошибке карточки")
if any(action == "card.error_report_112" for action, _at, _detail in card.dds_log):
return DeskOutcome()
card.dds_log.append(("card.error_report_112", now_utc(), command.text.strip()))
case "card.open": case "card.open":
if not dds_lesson or not self.open(command.card_id): if not dds_lesson or not self.open(command.card_id):
return _refused("Карточка отсутствует в текущей очереди") return _refused("Карточка отсутствует в текущей очереди")

View file

@ -436,6 +436,13 @@ class SessionState(PersistedSession):
phone_reports=list(card.phone_reports) if card is not None else [], phone_reports=list(card.phone_reports) if card is not None else [],
phone_lines=list(card.phone_lines) if card is not None else [], phone_lines=list(card.phone_lines) if card is not None else [],
phone_pending=card.phone_pending if card is not None else None, phone_pending=card.phone_pending if card is not None else None,
card_error_discovered=(card.scenario.dds_card_error is not None and any(
report.phase == "arrived" for report in card.phone_reports
)) if card is not None else False,
report_112_text=(next(
(detail for action, _at, detail in reversed(card.dds_log)
if action == "card.error_report_112"), None,
) if card is not None else None),
card_id=card.card_id if card is not None else None, card_id=card.card_id if card is not None else None,
card_index=(desk.card_index + 1 if card is not None card_index=(desk.card_index + 1 if card is not None
else min(desk.next_index + 1, len(desk.scenarios))), else min(desk.next_index + 1, len(desk.scenarios))),

View file

@ -2,7 +2,7 @@
import asyncio import asyncio
import time import time
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from uuid import uuid4 from uuid import uuid4
import pytest import pytest
@ -17,6 +17,7 @@ from app.domain.timers import TimerCode
from app.main import app from app.main import app
from app.scenarios import store from app.scenarios import store
from app.session.hub import hub from app.session.hub import hub
from app.session.checkpoint import dump_state, load_state
from app.session.store import MemorySessionStore from app.session.store import MemorySessionStore
@ -836,6 +837,76 @@ def send_status(station, service, status):
read_until(station, "station.state") read_until(station, "station.state")
def test_crew_discovers_card_address_error_and_dispatcher_reports_it_to_112(client):
session_id, control = start(client, scenario_id="dds-card-address-error")
try:
state = hub.get(session_id)
card = state.desk.active
assert card.dispatched_card.address == "улица Ленина, 16"
assert card.scenario.ground_truth.address == "улица Ленина, 14"
with client.websocket_connect(f"/ws/station/{session_id}") as station:
service = accept_with_crew(station)
station.send_json({
"type": "card.error_report_112", "card_id": str(card.card_id),
"text": "В карточке неверный адрес",
})
assert "Сначала получите доклад" in read_until(station, "error")["message"]
complete_phone_call(station, state, "dispatched")
read_until(station, "station.state")
send_status(station, service, "responding")
report = complete_phone_call(station, state, "arrived")
snapshot = read_until(station, "station.state")["snapshot"]
assert "улица Ленина, 14" in report["text"]
assert "улица Ленина, 16" in report["text"]
assert snapshot["card_error_discovered"] is True
assert snapshot["report_112_text"] is None
message = "В карточке дом 16; бригада на месте установила дом 14."
station.send_json({
"type": "card.error_report_112", "card_id": str(card.card_id), "text": message,
})
snapshot = read_until(station, "station.state")["snapshot"]
assert snapshot["report_112_text"] == message
station.send_json({
"type": "card.error_report_112", "card_id": str(card.card_id), "text": message,
})
assert sum(action == "card.error_report_112" for action, _, _ in card.dds_log) == 1
active = client.get("/api/sessions/active").json()
teacher = next(row for row in active if row["session_id"] == str(session_id))
assert teacher["dds_snapshot"]["report_112_text"] == message
restored = load_state(dump_state(state), datetime.now(UTC))
assert restored.station_snapshot().report_112_text == message
station.send_json({"type": "station.finish"})
read_until(station, "score.ready")
report = client.get(f"/api/sessions/{session_id}/report").json()
actions = report["card_results"][0]["actions"]
assert any(action["type"] == "card.error_report_112" and action["text"] == message
for action in actions)
assert message in client.get(f"/api/sessions/{session_id}/report.csv").text
finally:
control.__exit__(None, None, None)
def test_report_to_112_is_unavailable_without_planted_card_error(client):
session_id, control = start(client)
try:
state = hub.get(session_id)
with client.websocket_connect(f"/ws/station/{session_id}") as station:
read_until(station, "card.received")
snapshot = read_until(station, "station.state")["snapshot"]
assert snapshot["card_error_discovered"] is False
station.send_json({
"type": "card.error_report_112", "card_id": str(state.desk.active.card_id),
"text": "Сообщаю об ошибке карточки в 112",
})
assert "Сначала получите доклад" in read_until(station, "error")["message"]
assert not state.desk.active.dds_log
finally:
control.__exit__(None, None, None)
@pytest.mark.parametrize(("reaction", "passed"), [ @pytest.mark.parametrize(("reaction", "passed"), [
("fast", True), ("slow", False), ("missing", False), ("early", True), ("fast", True), ("slow", False), ("missing", False), ("early", True),
]) ])

View file

@ -16,7 +16,7 @@ from fastapi.testclient import TestClient
from app.api.auth import Principal from app.api.auth import Principal
from app.api.http import sessions from app.api.http import sessions
from app.config import get_settings from app.config import get_settings
from app.domain.events import SessionReport from app.domain.events import DdsCardReport, SessionReport
from app.domain.roles import Role from app.domain.roles import Role
from app.main import app from app.main import app
from app.scoring.export import _cell, certificate_pdf, to_csv, to_pdf from app.scoring.export import _cell, certificate_pdf, to_csv, to_pdf
@ -83,6 +83,31 @@ def test_csv_shows_enabled_report_reaction_limit():
assert ["Критерии", "", "Лимит реакции на доклад бригады, с", "20", ""] in rows assert ["Критерии", "", "Лимит реакции на доклад бригады, с", "20", ""] in rows
def test_card_error_message_to_112_is_in_csv_and_pdf(tmp_path):
report = sample_report()
message = "В карточке дом 16; по докладу бригады фактически дом 14."
report.card_results = [DdsCardReport(
card_id=uuid4(), scenario_id="dds-card-address-error", score_auto=100,
reply_text="", metrics=[], findings=[], actions=[{
"type": "card.error_report_112", "text": message,
"at": "2026-09-21T12:05:00+00:00",
}],
)]
rows = list(csv.reader(io.StringIO(to_csv(report).decode("utf-8-sig"))))
assert any(row[0] == "Сообщение в 112" and row[3] == message for row in rows)
import shutil
import subprocess
if shutil.which("pdftotext"):
path = tmp_path / "dds-error.pdf"
path.write_bytes(to_pdf(report))
extracted = subprocess.check_output(["pdftotext", str(path), "-"], text=True)
assert "Сообщения в 112 об ошибках карточек" in extracted
assert message in extracted
def test_pdf_supports_cyrillic_and_spans_pages(tmp_path): def test_pdf_supports_cyrillic_and_spans_pages(tmp_path):
data = to_pdf(sample_report(long=True)) data = to_pdf(sample_report(long=True))
assert data.startswith(b"%PDF-") assert data.startswith(b"%PDF-")

View file

@ -236,8 +236,8 @@ def test_dispatcher_metric_dds_ack_excludes_pause_from_elapsed(clock):
clock.advance(60) clock.advance(60)
state.resume() state.resume()
clock.advance(15) clock.advance(15)
card.set_service_status(service, ServiceStatus.ACCEPTED, "Принято")
card.on_event("dds.open") card.on_event("dds.open")
card.set_service_status(service, ServiceStatus.ACCEPTED, "Принято")
metric = next(m for m in dispatcher_metrics(card, 30_000, services=services) if m.key == "dds_ack") metric = next(m for m in dispatcher_metrics(card, 30_000, services=services) if m.key == "dds_ack")
assert metric.passed is True assert metric.passed is True

View file

@ -103,6 +103,8 @@ export function Debrief({ report, big = false }: { report: SessionReport; big?:
{card.address ? ` · ${card.address}` : ""}</summary> {card.address ? ` · ${card.address}` : ""}</summary>
{card.managed_service && <p><strong>Ответственная служба:</strong> {card.managed_service}</p>} {card.managed_service && <p><strong>Ответственная служба:</strong> {card.managed_service}</p>}
<p><strong>Ответ диспетчера:</strong> {card.reply_text || "не внесён"}</p> <p><strong>Ответ диспетчера:</strong> {card.reply_text || "не внесён"}</p>
{(card.actions ?? []).filter((action) => action.type === "card.error_report_112").map((action, actionIndex) =>
<p key={`report-112-${actionIndex}`}><strong>Сообщение об ошибке в 112:</strong> {String(action.text ?? "")}</p>)}
{card.metrics.some((metric) => !metric.passed || (metric.credit != null && metric.credit < 1)) && <ul> {card.metrics.some((metric) => !metric.passed || (metric.credit != null && metric.credit < 1)) && <ul>
{card.metrics.filter((metric) => !metric.passed || (metric.credit != null && metric.credit < 1)).map((metric) => {card.metrics.filter((metric) => !metric.passed || (metric.credit != null && metric.credit < 1)).map((metric) =>
<li key={metric.key} className={metric.passed ? "ref" : "state-violated"}> <li key={metric.key} className={metric.passed ? "ref" : "state-violated"}>

View file

@ -242,6 +242,7 @@ export function Dds() {
const [crewReplyText, setCrewReplyText] = useState(""); const [crewReplyText, setCrewReplyText] = useState("");
const [adjacentTarget, setAdjacentTarget] = useState(""); const [adjacentTarget, setAdjacentTarget] = useState("");
const [adjacentText, setAdjacentText] = useState(""); const [adjacentText, setAdjacentText] = useState("");
const [report112Text, setReport112Text] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [ended, setEnded] = useState(false); const [ended, setEnded] = useState(false);
const [paused, setPaused] = useState(false); const [paused, setPaused] = useState(false);
@ -295,6 +296,7 @@ export function Dds() {
setCrewReplyText(""); setCrewReplyText("");
setAdjacentTarget(""); setAdjacentTarget("");
setAdjacentText(""); setAdjacentText("");
setReport112Text("");
setZone(null); setZone(null);
setScoreReady(false); setScoreReady(false);
setView(openCardAfterSwitch.current ? "card" : "registry"); setView(openCardAfterSwitch.current ? "card" : "registry");
@ -719,6 +721,20 @@ export function Dds() {
{adjacentLines.map((line, index) => <p key={`${line.at}-${index}`}><small>{time(line.at)} · {line.speaker === "crew" ? line.crew : "Диспетчер ДДС"}</small><br />{line.text}</p>)} {adjacentLines.map((line, index) => <p key={`${line.at}-${index}`}><small>{time(line.at)} · {line.speaker === "crew" ? line.crew : "Диспетчер ДДС"}</small><br />{line.text}</p>)}
</div>} </div>}
</>} </>}
{station?.card_error_discovered && <div className="dds-phone-form">
<h3>Сообщение об ошибке карточки в 112</h3>
{station.report_112_text ? <p>Сообщено в 112: {station.report_112_text}</p> : <>
<p>После доклада бригады сообщите оператору 112, что указано в карточке и что установлено на месте.</p>
<label htmlFor="dds-report-112">Содержание сообщения</label>
<textarea id="dds-report-112" rows={3} value={report112Text}
disabled={!isConnected || ended || paused}
onChange={(event) => setReport112Text(event.target.value)} />
<button type="button" disabled={!cardId || !isConnected || ended || paused || report112Text.trim().length < 10}
onClick={() => send({ type: "card.error_report_112", card_id: cardId, text: report112Text })}>
Зафиксировать сообщение в 112
</button>
</>}
</div>}
</section>} </section>}
{pane === "reply" && <section className="dds-panel dds-reply"> {pane === "reply" && <section className="dds-panel dds-reply">
<div className="dds-panel-title"><h2>Рабочая заметка диспетчера</h2> <div className="dds-panel-title"><h2>Рабочая заметка диспетчера</h2>

View file

@ -393,6 +393,9 @@ export function Instructor() {
<small>{report.text}</small> <small>{report.text}</small>
</p>)} </p>)}
</div> : null} </div> : null}
{item.dds_snapshot.report_112_text && <p className="active-session-log">
<strong>Сообщено в 112 об ошибке карточки:</strong> {item.dds_snapshot.report_112_text}
</p>}
</details>}</td> </details>}</td>
<td>{item.exercise === "dds" ? `Открытых карточек: ${item.dds_open_cards}` : "—"}</td> <td>{item.exercise === "dds" ? `Открытых карточек: ${item.dds_open_cards}` : "—"}</td>
<td className={item.dds_overdue_cards ? "state-violated" : "state-ok"}> <td className={item.dds_overdue_cards ? "state-violated" : "state-ok"}>

View file

@ -108,6 +108,13 @@ export interface CardBriefing {
handoff_to_dds?: boolean; handoff_to_dds?: boolean;
} }
/** Сообщение диспетчера в 112 после доклада бригады с места. */
export interface CardErrorReport112 {
type: "card.error_report_112";
card_id: string;
text: string;
}
export interface CardNext { export interface CardNext {
type: "card.next"; type: "card.next";
card_id: string; card_id: string;
@ -719,6 +726,8 @@ export interface StationSnapshot {
phone_reports?: Array<PhoneReportRecord>; phone_reports?: Array<PhoneReportRecord>;
phone_lines?: Array<PhoneLineRecord>; phone_lines?: Array<PhoneLineRecord>;
phone_pending?: PhoneCallPending | null; phone_pending?: PhoneCallPending | null;
card_error_discovered?: boolean;
report_112_text?: string | null;
card_id?: string | null; card_id?: string | null;
card_index?: number; card_index?: number;
card_total?: number; card_total?: number;
@ -847,7 +856,7 @@ export type InstructorToServer = ScenarioStart | DirectorInject | ReferencePlay
export type ServerToStation = CardReceived | StationState | PhoneLine | PhoneReport | TimerTick | SessionEnded | ScoreReady | CommandAck | SessionPaused | ErrorEvent; export type ServerToStation = CardReceived | StationState | PhoneLine | PhoneReport | TimerTick | SessionEnded | ScoreReady | CommandAck | SessionPaused | ErrorEvent;
export type StationToServer = CardAck | CardBounce | ServiceStatusSet | CrewSelect | PhoneDial | PhoneBrief | PhoneCheck | PhoneHangup | PhoneReply | AdjacentDial | StationFinish | CardReply | CardNext | CardOpen | ZoneDecision | CrewDispatched | CrewArrived; export type StationToServer = CardAck | CardBounce | ServiceStatusSet | CrewSelect | PhoneDial | PhoneBrief | PhoneCheck | PhoneHangup | PhoneReply | AdjacentDial | StationFinish | CardReply | CardErrorReport112 | CardNext | CardOpen | ZoneDecision | CrewDispatched | CrewArrived;
export type EventName = export type EventName =
"adjacent.dial" "adjacent.dial"
@ -864,6 +873,7 @@ export type EventName =
| "card.ack" | "card.ack"
| "card.bounce" | "card.bounce"
| "card.briefing" | "card.briefing"
| "card.error_report_112"
| "card.next" | "card.next"
| "card.open" | "card.open"
| "card.received" | "card.received"

View file

@ -0,0 +1,40 @@
# Учебная ошибка уже переданной КИО: бригада обнаруживает верный адрес на месте.
# Курсанту ДДС нужно сообщить расхождение оператору 112; автооценки этого шага нет.
id: dds-card-address-error
title: "Ошибка адреса в готовой карточке пожара"
type: fire
level: L2
topics: [fires]
modes: [training]
signs: [жилой дом, балкон, открытое пламя]
persona:
base: worried
arc:
- { stage: registration, mood: worried }
first_line: "На балконе пожар, приезжайте на улицу Ленина, дом 14."
facts:
- id: f_address
value: "улица Ленина, 14, квартира 47"
reveal_on: { question: q_address }
- id: f_what_burns
value: "горит балкон, жильцы выходят из квартиры"
reveal_on: { question: q_what_burns }
checklist:
- { id: q_address, fact: f_address, question: "Уточните адрес происшествия" }
- { id: q_what_burns, fact: f_what_burns, question: "Что именно горит?" }
required_fields: [address, incident_type, description]
ground_truth:
address: "улица Ленина, 14"
victims: 0
dds_card_error:
field: address
card_value: "улица Ленина, 16"
tree:
pregenerated: false