feat: ж/д слой + cant_swim в скоринг + профили вне модели

Определение специфических рекомендаций (матрица профилей §8):

1. Ж/д слой (закрыт мёртвый railway ×2.5 у РАС):
   - /api/v1/water/{case_id} отдаёт railway=rail как LineString
     (без service/industrial/military веток), кэш общий v2;
   - railway_warning «перекрыть/проверить немедленно» по профилям;
   - SearchMap: Polyline слой ж/д (тёмно-красный), счётчики 💧/🚂.

2. cant_swim → профиль не_умеет_плавать (water ×3.0, без изменения
   радиуса, critical_warning «обследовать водоёмы НЕМЕДЛЕННО»):
   - раньше чекбокс влиял только на текст, в скоринге был пробел;
   - derive в analyze._derive_profiles — работает и для closed_cases.

3. unmodeled_profiles: ДЦП/слабое зрение/слух — честная пометка
   «вне поведенческой модели» с пояснением (vector_tasks B12:
   профили без аналога не выдавать за учтённые); блок на фронте
   в карточке здоровья.

Площадь воды: сферический эксцесс, проверен на квадрате 53° (744017 м²
vs 743272 точного). Тесты: 202 passed (новый test_cant_swim_profile).
This commit is contained in:
2026-09-09 13:12:55 +03:00
parent 98e8e58023
commit f20080305d
2069 changed files with 803865 additions and 97 deletions
@@ -0,0 +1,357 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
from __future__ import annotations
import abc
import sys
from cryptography.hazmat.bindings._rust import openssl as rust_openssl
from cryptography.utils import Buffer
__all__ = [
"MD5",
"SHA1",
"SHA3_224",
"SHA3_256",
"SHA3_384",
"SHA3_512",
"SHA224",
"SHA256",
"SHA384",
"SHA512",
"SHA512_224",
"SHA512_256",
"SHAKE128",
"SHAKE256",
"SM3",
"BLAKE2b",
"BLAKE2s",
"ExtendableOutputFunction",
"Hash",
"HashAlgorithm",
"HashContext",
"XOFHash",
]
class HashAlgorithm(metaclass=abc.ABCMeta):
@property
@abc.abstractmethod
def name(self) -> str:
"""
A string naming this algorithm (e.g. "sha256", "md5").
"""
@property
@abc.abstractmethod
def digest_size(self) -> int:
"""
The size of the resulting digest in bytes.
"""
@property
@abc.abstractmethod
def block_size(self) -> int | None:
"""
The internal block size of the hash function, or None if the hash
function does not use blocks internally (e.g. SHA3).
"""
class HashContext(metaclass=abc.ABCMeta):
@property
@abc.abstractmethod
def algorithm(self) -> HashAlgorithm:
"""
A HashAlgorithm that will be used by this context.
"""
@abc.abstractmethod
def update(self, data: Buffer) -> None:
"""
Processes the provided bytes through the hash.
"""
@abc.abstractmethod
def finalize(self) -> bytes:
"""
Finalizes the hash context and returns the hash digest as bytes.
"""
@abc.abstractmethod
def copy(self) -> HashContext:
"""
Return a HashContext that is a copy of the current context.
"""
Hash = rust_openssl.hashes.Hash
HashContext.register(Hash)
XOFHash = rust_openssl.hashes.XOFHash
class ExtendableOutputFunction(metaclass=abc.ABCMeta):
"""
An interface for extendable output functions.
"""
class SHA1(HashAlgorithm):
name = "sha1"
digest_size = 20
block_size = 64
def __eq__(self, other: object) -> bool:
if not isinstance(other, SHA1):
return NotImplemented
return True
class SHA512_224(HashAlgorithm): # noqa: N801
name = "sha512-224"
digest_size = 28
block_size = 128
def __eq__(self, other: object) -> bool:
if not isinstance(other, SHA512_224):
return NotImplemented
return True
class SHA512_256(HashAlgorithm): # noqa: N801
name = "sha512-256"
digest_size = 32
block_size = 128
def __eq__(self, other: object) -> bool:
if not isinstance(other, SHA512_256):
return NotImplemented
return True
class SHA224(HashAlgorithm):
name = "sha224"
digest_size = 28
block_size = 64
def __eq__(self, other: object) -> bool:
if not isinstance(other, SHA224):
return NotImplemented
return True
class SHA256(HashAlgorithm):
name = "sha256"
digest_size = 32
block_size = 64
def __eq__(self, other: object) -> bool:
if not isinstance(other, SHA256):
return NotImplemented
return True
class SHA384(HashAlgorithm):
name = "sha384"
digest_size = 48
block_size = 128
def __eq__(self, other: object) -> bool:
if not isinstance(other, SHA384):
return NotImplemented
return True
class SHA512(HashAlgorithm):
name = "sha512"
digest_size = 64
block_size = 128
def __eq__(self, other: object) -> bool:
if not isinstance(other, SHA512):
return NotImplemented
return True
class SHA3_224(HashAlgorithm): # noqa: N801
name = "sha3-224"
digest_size = 28
block_size = None
def __eq__(self, other: object) -> bool:
if not isinstance(other, SHA3_224):
return NotImplemented
return True
class SHA3_256(HashAlgorithm): # noqa: N801
name = "sha3-256"
digest_size = 32
block_size = None
def __eq__(self, other: object) -> bool:
if not isinstance(other, SHA3_256):
return NotImplemented
return True
class SHA3_384(HashAlgorithm): # noqa: N801
name = "sha3-384"
digest_size = 48
block_size = None
def __eq__(self, other: object) -> bool:
if not isinstance(other, SHA3_384):
return NotImplemented
return True
class SHA3_512(HashAlgorithm): # noqa: N801
name = "sha3-512"
digest_size = 64
block_size = None
def __eq__(self, other: object) -> bool:
if not isinstance(other, SHA3_512):
return NotImplemented
return True
class SHAKE128(HashAlgorithm, ExtendableOutputFunction):
name = "shake128"
block_size = None
def __init__(self, digest_size: int):
if not isinstance(digest_size, int):
raise TypeError("digest_size must be an integer")
if digest_size < 1:
raise ValueError("digest_size must be a positive integer")
self._digest_size = digest_size
def __eq__(self, other: object) -> bool:
if not isinstance(other, SHAKE128):
return NotImplemented
return self._digest_size == other._digest_size
@property
def digest_size(self) -> int:
return self._digest_size
@classmethod
def xof(cls):
return cls(sys.maxsize)
class SHAKE256(HashAlgorithm, ExtendableOutputFunction):
name = "shake256"
block_size = None
def __init__(self, digest_size: int):
if not isinstance(digest_size, int):
raise TypeError("digest_size must be an integer")
if digest_size < 1:
raise ValueError("digest_size must be a positive integer")
self._digest_size = digest_size
def __eq__(self, other: object) -> bool:
if not isinstance(other, SHAKE256):
return NotImplemented
return self._digest_size == other._digest_size
@property
def digest_size(self) -> int:
return self._digest_size
@classmethod
def xof(cls):
return cls(sys.maxsize)
class MD5(HashAlgorithm):
name = "md5"
digest_size = 16
block_size = 64
def __eq__(self, other: object) -> bool:
if not isinstance(other, MD5):
return NotImplemented
return True
class BLAKE2b(HashAlgorithm):
name = "blake2b"
_max_digest_size = 64
_min_digest_size = 1
block_size = 128
def __init__(self, digest_size: int):
if digest_size != 64:
raise ValueError("Digest size must be 64")
self._digest_size = digest_size
def __eq__(self, other: object) -> bool:
if not isinstance(other, BLAKE2b):
return NotImplemented
return self._digest_size == other._digest_size
@property
def digest_size(self) -> int:
return self._digest_size
class BLAKE2s(HashAlgorithm):
name = "blake2s"
block_size = 64
_max_digest_size = 32
_min_digest_size = 1
def __init__(self, digest_size: int):
if digest_size != 32:
raise ValueError("Digest size must be 32")
self._digest_size = digest_size
def __eq__(self, other: object) -> bool:
if not isinstance(other, BLAKE2s):
return NotImplemented
return self._digest_size == other._digest_size
@property
def digest_size(self) -> int:
return self._digest_size
class SM3(HashAlgorithm):
name = "sm3"
digest_size = 32
block_size = 64
def __eq__(self, other: object) -> bool:
if not isinstance(other, SM3):
return NotImplemented
return True