release: dev → main (сдача хакатона) #106

Merged
Eleboba merged 128 commits from dev into main 2026-10-04 23:00:56 +03:00
7 changed files with 242 additions and 21 deletions
Showing only changes of commit 57b5ad1eb2 - Show all commits
+59
View File
@@ -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")))
+61
View File
@@ -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
View File
@@ -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
+17 -4
View File
@@ -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"
+58
View File
@@ -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)