feat(core): add verified Russian C-SSRS Recent source content
CI / ruff · pytest (pull_request) Successful in 31s
CI / ruff · pytest (pull_request) Successful in 31s
This commit is contained in:
@@ -0,0 +1,59 @@
|
|||||||
|
"""Russian Recent source transcription, separate from the legacy sequential scale.
|
||||||
|
|
||||||
|
This content does not authorize administering or continuing the questionnaire.
|
||||||
|
Use questionnaire.score_cssrs_recent for required/missing items and risk, and
|
||||||
|
evaluate_intake for the clinical policy gate before presenting another question.
|
||||||
|
Never render interviewer guidance as if it were another patient question.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||||
|
|
||||||
|
from app.config import get_settings
|
||||||
|
|
||||||
|
ItemId = Literal["q1", "q2", "q3", "q4", "q5", "q6_lifetime", "q6_recent"]
|
||||||
|
|
||||||
|
|
||||||
|
class SourceModel(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CssrsOption(SourceModel):
|
||||||
|
value: int = Field(strict=True, ge=0, le=1)
|
||||||
|
label: str
|
||||||
|
|
||||||
|
|
||||||
|
class CssrsItem(SourceModel):
|
||||||
|
id: ItemId
|
||||||
|
text: str = Field(min_length=1)
|
||||||
|
interviewer_guidance: str | None
|
||||||
|
recall_period: Literal["past_month", "lifetime", "past_3_months"]
|
||||||
|
yes_risk: Literal["low", "moderate", "high"]
|
||||||
|
|
||||||
|
|
||||||
|
class CssrsRussianForm(SourceModel):
|
||||||
|
id: Literal["cssrs_recent_ru"]
|
||||||
|
version: str = Field(min_length=1)
|
||||||
|
title: str
|
||||||
|
subtitle: Literal["Screen Version - Recent"]
|
||||||
|
period_label_original: str
|
||||||
|
initial_instruction: str
|
||||||
|
branch_instruction: str
|
||||||
|
options: tuple[CssrsOption, CssrsOption]
|
||||||
|
items: tuple[CssrsItem, ...]
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def _structure(self) -> "CssrsRussianForm":
|
||||||
|
if tuple(item.id for item in self.items) != ("q1", "q2", "q3", "q4", "q5", "q6_lifetime", "q6_recent"):
|
||||||
|
raise ValueError("Recent form requires seven ordered answer fields")
|
||||||
|
if tuple(option.value for option in self.options) != (1, 0):
|
||||||
|
raise ValueError("source answer columns are Yes, No")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def load_cssrs_russian(path: Path | None = None) -> CssrsRussianForm:
|
||||||
|
path = path or get_settings().config_dir / "instruments" / "cssrs_recent_ru.yaml"
|
||||||
|
return CssrsRussianForm.model_validate(yaml.safe_load(path.read_text(encoding="utf-8")))
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Verbatim source content; only whitespace/line wraps normalized. Not enabled in the legacy dialog.
|
||||||
|
id: cssrs_recent_ru
|
||||||
|
version: source-2026-10-03
|
||||||
|
title: КОЛУМБИЙСКАЯ ШКАЛА ОЦЕНКИ ТЯЖЕСТИ СУИЦИДА
|
||||||
|
subtitle: Screen Version - Recent
|
||||||
|
period_label_original: Последние 1 месяца
|
||||||
|
initial_instruction: Задайте вопросы 1 и 2
|
||||||
|
branch_instruction: ЕСЛИ ДА 2, Задайте вопросы 3, 4, 5 и 6. ЕСЛИ Нет, Задайте вопросы 6.
|
||||||
|
options:
|
||||||
|
- value: 1
|
||||||
|
label: Да
|
||||||
|
- value: 0
|
||||||
|
label: Нет
|
||||||
|
items:
|
||||||
|
- id: q1
|
||||||
|
text: Возникало ли у Вас желание умереть или заснуть и не просыпаться?
|
||||||
|
interviewer_guidance: null
|
||||||
|
recall_period: past_month
|
||||||
|
yes_risk: low
|
||||||
|
- id: q2
|
||||||
|
text: Думали ли Вы в самом деле о том, чтобы убить себя?
|
||||||
|
interviewer_guidance: null
|
||||||
|
recall_period: past_month
|
||||||
|
yes_risk: low
|
||||||
|
- id: q3
|
||||||
|
text: Думали ли Вы о том, как Вы могли бы это сделать?
|
||||||
|
interviewer_guidance: 'Пациент подтверждает, что его посещают мысли о самоубийстве и что в течение периода наблюдения
|
||||||
|
он/она думал(-а) хотя бы об одном способе самоубийства. Данная ситуация отлична от такой, когда пациент разработал
|
||||||
|
конкретный план: время, место или детали метода самоубийства (например, пациент думал о способе самоубийства,
|
||||||
|
но не о конкретном плане). Сюда относятся люди, которые скажут: “Я думал(-а) о передозировке таблеток, но так
|
||||||
|
и не построил(-а) конкретного плана о том, когда, где и как осуществить это…..и я бы никогда не довёл(-а) дело
|
||||||
|
до конца”.'
|
||||||
|
recall_period: past_month
|
||||||
|
yes_risk: moderate
|
||||||
|
- id: q4
|
||||||
|
text: Посещали ли Вас подобные мысли, и было ли у Вас какое-нибудь намерение действовать в соответствии с ними?
|
||||||
|
interviewer_guidance: 'Активные мысли о самоубийстве при заявлении пациента о некотором намерении действовать
|
||||||
|
в соответствии с этими мыслями, в отличие от подобного утверждения: “Такие мысли посещают меня, но я определенно
|
||||||
|
не стану ничего предпринимать”.'
|
||||||
|
recall_period: past_month
|
||||||
|
yes_risk: high
|
||||||
|
- id: q5
|
||||||
|
text: Начали ли Вы разрабатывать или уже разработали детальный план самоубийства? Намереваетесь ли Вы осуществить
|
||||||
|
этот план?
|
||||||
|
interviewer_guidance: null
|
||||||
|
recall_period: past_month
|
||||||
|
yes_risk: high
|
||||||
|
- id: q6_lifetime
|
||||||
|
text: Вы когда-нибудь что-нибудь делали, начинали что-то делать или готовились сделать что-нибудь, чтобы покончить
|
||||||
|
с собой?
|
||||||
|
interviewer_guidance: 'Примеры: запаслись таблетками, приобрели пистолет, раздали ценные вещи, написали завещание
|
||||||
|
или предсмертную записку, запаслись таблетками, но не проглотили их, держали пистолет, но передумали стрелять
|
||||||
|
или его кто-то выхватил из вашей руки, поднялись на крышу, но не прыгнули; или принимали таблетки, пытались
|
||||||
|
застрелиться, порезались, пытался повеситься и т. д.'
|
||||||
|
recall_period: lifetime
|
||||||
|
yes_risk: moderate
|
||||||
|
- id: q6_recent
|
||||||
|
text: ЕСЛИ ДА, то было ли это в последние 3 месяца?
|
||||||
|
interviewer_guidance: null
|
||||||
|
recall_period: past_3_months
|
||||||
|
yes_risk: high
|
||||||
+46
-17
@@ -60,30 +60,57 @@ RUS-AUDIT: © ВОЗ, 2021; [отчёт ВОЗ](https://www.who.int/europe/publi
|
|||||||
CC BY-NC-SA 3.0 IGO. Использование в этом пакете — некоммерческий прототип.
|
CC BY-NC-SA 3.0 IGO. Использование в этом пакете — некоммерческий прототип.
|
||||||
Наличие шкалы здесь не означает одобрения продукта ВОЗ или авторами.
|
Наличие шкалы здесь не означает одобрения продукта ВОЗ или авторами.
|
||||||
|
|
||||||
## C-SSRS: источник и незавершённая часть
|
## C-SSRS: русский бланк Recent
|
||||||
|
|
||||||
Официальная [страница переводов Columbia](https://cssrs.columbia.edu/the-columbia-scale-c-ssrs/translations/)
|
Официальная [страница переводов Columbia](https://cssrs.columbia.edu/the-columbia-scale-c-ssrs/translations/)
|
||||||
ссылается на [Dropbox Screeners](https://www.dropbox.com/scl/fo/pkg1gwxxn1266rke8oite/AC2UFfOKCtefU-o9EEXP-yk?dl=0&rlkey=4grouuqbchfa8obxlcb3msnpz).
|
ссылается на [Dropbox Screeners](https://www.dropbox.com/scl/fo/pkg1gwxxn1266rke8oite/AC2UFfOKCtefU-o9EEXP-yk?dl=0&rlkey=4grouuqbchfa8obxlcb3msnpz).
|
||||||
Ожидаемый файл: `C-SSRS Screener - Russian.pdf`. **Файл не получен, официальный
|
3 октября пользователь предоставил локальный `C-SSRS Screener - Russian.pdf`.
|
||||||
русский текст не добавлен**: проверка административной политики браузера не
|
Неизменённая копия — `cssrs-screener-russian.pdf`, одна страница, заголовок
|
||||||
доступна; обход блокировки не выполнялся.
|
**Screen Version - Recent**. Вопросы, пояснения, периоды, варианты и цветовые
|
||||||
|
категории сверены с отрисованной страницей. Хеши оригинала и извлечённого текста
|
||||||
|
сохранены в `review-status.json`. Файл получен от пользователя после блокировки
|
||||||
|
загрузки; совпадение байтов с удалённой копией Dropbox независимо не проверено.
|
||||||
|
|
||||||
|
`config/instruments/cssrs_recent_ru.yaml` содержит отдельную транскрипцию,
|
||||||
|
`app/core/cssrs_content.py` — типизированный загрузчик `load_cssrs_russian`.
|
||||||
|
Версия `source-2026-10-03` — локальная версия содержания, **не номер редакции
|
||||||
|
Columbia**. Даты в метаданных PDF не используются как дата клинической редакции.
|
||||||
|
Сохранены все формулировки и опечатки, включая «Последние 1 месяца»,
|
||||||
|
«ЕСЛИ ДА 2», «пытался повеситься» и повтор «запаслись таблетками».
|
||||||
|
Нормализованы только межсловные пробелы и переносы. Пояснения интервьюеру
|
||||||
|
хранятся отдельно от вопросов. Любое редактирование требует отдельного решения.
|
||||||
|
|
||||||
|
Переходы по бланку: q1/q2 всем; при q2=0 перейти к q6; при q2=1 спросить q3–q5;
|
||||||
|
q6 за всю жизнь всем; при положительном q6 уточнить последние три месяца.
|
||||||
|
Для q1–q5 период — последний месяц. Ответы «Да»/«Нет» кодируются 1/0;
|
||||||
|
это коды ответов, **не суммируемые баллы**.
|
||||||
|
|
||||||
|
| Положительный ответ | Цвет бланка | Категория |
|
||||||
|
|---|---|---|
|
||||||
|
| q1, q2 | жёлтый | low |
|
||||||
|
| q3, q6_lifetime | оранжевый | moderate |
|
||||||
|
| q4, q5, q6_recent | красный | high |
|
||||||
|
|
||||||
|
При нескольких положительных ответах используется максимальная категория.
|
||||||
|
Поведение ранее трёх месяцев не превращается в «риска нет».
|
||||||
|
Правила совпадают с [Recent Screener для первичной помощи](https://cssrs.columbia.edu/wp-content/uploads/C-SSRS-Screener-with-Triage-Points-for-Primary-Care-2026.pdf)
|
||||||
|
и [описанием триажа Columbia](https://cssrs.columbia.edu/the-columbia-scale-c-ssrs/risk-identification/).
|
||||||
|
Неполные или противоречивые данные не получают итоговую категорию;
|
||||||
|
известный сигнал сохраняется как нижняя граница риска.
|
||||||
|
|
||||||
`config/scales/cssrs_screen.yaml` остаётся историческим демо-переводом. Его q6
|
`config/scales/cssrs_screen.yaml` остаётся историческим демо-переводом. Его q6
|
||||||
спрашивает только о последних трёх месяцах; автоматически конвертировать такой
|
спрашивает только о последних трёх месяцах; автоматически конвертировать такой
|
||||||
ответ в пожизненный нельзя. Не выдавать этот файл за официальный перевод.
|
ответ в пожизненный нельзя. Новый бланк расположен вне `config/scales`, поскольку
|
||||||
|
действующий последовательный диалог не поддерживает его условные переходы.
|
||||||
|
Он ещё не включён в пациентский сценарий. Загрузчик текста сам по себе не
|
||||||
|
разрешает продолжение опроса: адаптер должен учитывать `evaluate_intake`,
|
||||||
|
а порядок обязательных вопросов получать из `score_cssrs_recent`.
|
||||||
|
|
||||||
Новая структура данных использует q1…q5, q6_lifetime и q6_recent. Проверена по
|
Бланк не разрешает противоречие об остановке при любом «Да»: C01 остаётся
|
||||||
[официальному Recent Screener для первичной помощи](https://cssrs.columbia.edu/wp-content/uploads/C-SSRS-Screener-with-Triage-Points-for-Primary-Care-2026.pdf)
|
решением врача. Также нужны C06 (возраст/популяция, формат применения, обучение
|
||||||
и [описанию триажа Columbia](https://cssrs.columbia.edu/the-columbia-scale-c-ssrs/risk-identification/):
|
и повторные замеры) и согласование пациентского сценария. Версию «с последнего
|
||||||
q1/q2 всем; при q2=0 перейти к q6; при q2=1 спросить q3–q5;
|
контакта» нельзя получить простой заменой заголовка Recent. Статус лицензии и
|
||||||
q6 за всю жизнь всем; при положительном q6 уточнить последние три месяца.
|
применимость к коммерческому продукту подтверждаются отдельно.
|
||||||
Поведение ранее трёх месяцев учитывается отдельно и не становится «риска нет».
|
|
||||||
|
|
||||||
Для завершения: получить русский PDF из официальной папки, сохранить хеш/дату/
|
|
||||||
версию, посимвольно сверить вопросы, пояснения, периоды и варианты, проверить
|
|
||||||
переходы на отрисованном бланке, затем добавить отдельную версию русской шкалы.
|
|
||||||
Версию «с последнего контакта» нельзя получить простой заменой заголовка Recent.
|
|
||||||
Статус лицензии и применимость к коммерческому продукту подтверждаются отдельно.
|
|
||||||
|
|
||||||
## Независимый обратный перевод
|
## Независимый обратный перевод
|
||||||
|
|
||||||
@@ -153,3 +180,5 @@ RUS-AUDIT-S для каждого пола, границы полной форм
|
|||||||
ветви C-SSRS, давнее поведение, пропуски/противоречия, кризис на неполном опросе,
|
ветви C-SSRS, давнее поведение, пропуски/противоречия, кризис на неполном опросе,
|
||||||
конфликт PHQ-9/9 и C-SSRS, пороги маршрутов, независимые направления и отсутствие
|
конфликт PHQ-9/9 и C-SSRS, пороги маршрутов, независимые направления и отсутствие
|
||||||
разрешения на автоматический переход без реального решения врача.
|
разрешения на автоматический переход без реального решения врача.
|
||||||
|
Тесты C-SSRS дополнительно восстанавливают полный текст страницы из структуры,
|
||||||
|
проверяют хеши и сверяют все комбинации ветвей с цветовыми категориями бланка.
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
КОЛУМБИЙСКАЯ ШКАЛА ОЦЕНКИ ТЯЖЕСТИ СУИЦИДА Screen Version - Recent Последние 1 месяца Задайте вопросы 1 и 2 Да Нет 1. Возникало ли у Вас желание умереть или заснуть и не просыпаться? 2. Думали ли Вы в самом деле о том, чтобы убить себя? ЕСЛИ ДА 2, Задайте вопросы 3, 4, 5 и 6. ЕСЛИ Нет, Задайте вопросы 6. 3. Думали ли Вы о том, как Вы могли бы это сделать? Пациент подтверждает, что его посещают мысли о самоубийстве и что в течение периода наблюдения он/она думал(-а) хотя бы об одном способе самоубийства. Данная ситуация отлична от такой, когда пациент разработал конкретный план: время, место или детали метода самоубийства (например, пациент думал о способе самоубийства, но не о конкретном плане). Сюда относятся люди, которые скажут: “Я думал(-а) о передозировке таблеток, но так и не построил(-а) конкретного плана о том, когда, где и как осуществить это…..и я бы никогда не довёл(-а) дело до конца”. 4. Посещали ли Вас подобные мысли, и было ли у Вас какое-нибудь намерение действовать в соответствии с ними? Активные мысли о самоубийстве при заявлении пациента о некотором намерении действовать в соответствии с этими мыслями, в отличие от подобного утверждения: “Такие мысли посещают меня, но я определенно не стану ничего предпринимать”. 5. Начали ли Вы разрабатывать или уже разработали детальный план самоубийства? Намереваетесь ли Вы осуществить этот план? 6. Вы когда-нибудь что-нибудь делали, начинали что-то делать или готовились сделать что-нибудь, чтобы покончить с собой? Примеры: запаслись таблетками, приобрели пистолет, раздали ценные вещи, написали завещание или предсмертную записку, запаслись таблетками, но не проглотили их, держали пистолет, но передумали стрелять или его кто-то выхватил из вашей руки, поднялись на крышу, но не прыгнули; или принимали таблетки, пытались застрелиться, порезались, пытался повеситься и т. д. ЕСЛИ ДА, то было ли это в последние 3 месяца? Low Risk Moderate Risk High Risk
|
||||||
@@ -6,11 +6,24 @@
|
|||||||
"clinician_approval": null,
|
"clinician_approval": null,
|
||||||
"phone_checks": "not_performed",
|
"phone_checks": "not_performed",
|
||||||
"cssrs_russian_pdf": {
|
"cssrs_russian_pdf": {
|
||||||
"expected_filename": "C-SSRS Screener - Russian.pdf",
|
"original_filename": "C-SSRS Screener - Russian.pdf",
|
||||||
"downloaded": false,
|
"received": true,
|
||||||
"text_verified": false,
|
"acquisition": "user_provided_local_file",
|
||||||
|
"received_on": "2026-10-03",
|
||||||
|
"file": "cssrs-screener-russian.pdf",
|
||||||
|
"sha256": "d1dc77068b73e83a0e7959681b0fe9f15775a8b99863cf0e11794308a4c77c85",
|
||||||
|
"source_text": "cssrs-screener-russian.source.txt",
|
||||||
|
"source_text_sha256": "89835360bd59c6ead1a56bec6f40c46b72f81165366c92f85ba5d25291748de7",
|
||||||
|
"text_verified": true,
|
||||||
|
"visual_verification": true,
|
||||||
|
"pages": 1,
|
||||||
|
"form_subtitle": "Screen Version - Recent",
|
||||||
|
"content_version": "source-2026-10-03",
|
||||||
|
"publisher_version": null,
|
||||||
"source": "https://cssrs.columbia.edu/the-columbia-scale-c-ssrs/translations/",
|
"source": "https://cssrs.columbia.edu/the-columbia-scale-c-ssrs/translations/",
|
||||||
"blocker": "Browser admin-enforced security check unavailable; not bypassed"
|
"remote_bytes_independently_verified": false,
|
||||||
|
"clinical_use_approved": false,
|
||||||
|
"note": "User supplied the file after the official-source download workflow was blocked. Local content and colors checked; publisher version is not inferred from PDF metadata."
|
||||||
},
|
},
|
||||||
"intake_release_ready": false,
|
"intake_release_ready": false,
|
||||||
"policy_version": "mvp-review-2026-10-03"
|
"policy_version": "mvp-review-2026-10-03"
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""Protect the reviewed PDF transcription and its connection to Recent scoring."""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from itertools import product
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from app.core.cssrs_content import load_cssrs_russian
|
||||||
|
from app.core.questionnaire import score_cssrs_recent
|
||||||
|
|
||||||
|
CLINICAL = Path(__file__).resolve().parents[1] / "docs" / "clinical"
|
||||||
|
|
||||||
|
|
||||||
|
def test_source_hashes_and_complete_transcription():
|
||||||
|
form = load_cssrs_russian()
|
||||||
|
manifest = json.loads((CLINICAL / "review-status.json").read_text())["cssrs_russian_pdf"]
|
||||||
|
assert hashlib.sha256((CLINICAL / manifest["file"]).read_bytes()).hexdigest() == manifest["sha256"]
|
||||||
|
source = (CLINICAL / manifest["source_text"]).read_bytes()
|
||||||
|
assert hashlib.sha256(source).hexdigest() == manifest["source_text_sha256"]
|
||||||
|
assert form.version == manifest["content_version"]
|
||||||
|
# Reconstruct the complete extracted page, including numbering, guidance,
|
||||||
|
# original typos, answer order and the color legend. No wording is dropped.
|
||||||
|
blocks = [form.title, form.subtitle, form.period_label_original, form.initial_instruction]
|
||||||
|
blocks.extend(option.label for option in form.options)
|
||||||
|
for index, item in enumerate(form.items):
|
||||||
|
blocks.append(f"{index + 1}. {item.text}" if index < 6 else item.text)
|
||||||
|
if item.interviewer_guidance:
|
||||||
|
blocks.append(item.interviewer_guidance)
|
||||||
|
if item.id == "q2":
|
||||||
|
blocks.append(form.branch_instruction)
|
||||||
|
blocks.extend(["Low Risk", "Moderate Risk", "High Risk"])
|
||||||
|
assert " ".join(blocks) == source.decode().strip()
|
||||||
|
assert [item.recall_period for item in form.items] == ["past_month"] * 5 + ["lifetime", "past_3_months"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_source_branches_and_color_categories_match_scorer():
|
||||||
|
form = load_cssrs_russian()
|
||||||
|
colors = {item.id: item.yes_risk for item in form.items}
|
||||||
|
assert list(colors.values()) == ["low", "low", "moderate", "high", "high", "moderate", "high"]
|
||||||
|
levels = ["none", "low", "moderate", "high"]
|
||||||
|
for values in product((0, 1), repeat=7):
|
||||||
|
all_answers = dict(zip(colors, values, strict=True))
|
||||||
|
asked = ["q1", "q2"]
|
||||||
|
if all_answers["q2"]:
|
||||||
|
asked.extend(["q3", "q4", "q5"])
|
||||||
|
asked.append("q6_lifetime")
|
||||||
|
if all_answers["q6_lifetime"]:
|
||||||
|
asked.append("q6_recent")
|
||||||
|
answers = {}
|
||||||
|
for item_id in asked:
|
||||||
|
partial = score_cssrs_recent(answers)
|
||||||
|
assert not partial.complete
|
||||||
|
assert partial.missing[0] == item_id
|
||||||
|
answers[item_id] = all_answers[item_id]
|
||||||
|
result = score_cssrs_recent(answers)
|
||||||
|
expected = max((levels.index(colors[key]) for key, value in answers.items() if value), default=0)
|
||||||
|
assert result.complete and result.risk == levels[expected]
|
||||||
|
assert result.required == tuple(asked)
|
||||||
Reference in New Issue
Block a user