fix(generator): правки по линии ревью Кодекса — зёрна, словарь, сторожа

- Зачем:
  - третья линия ревью (Кодекс, другое семейство моделей) нашла дыру в
    стороже неизменности когорты, отставший словарь и тест, который
    сторожил не тот адрес дерева зёрен.
- Что:
  - когорта отдаётся видом на замороженный массив: флаг только для
    чтения вызывающий мог снять и испортить память, которой пользуются
    все дни окна. Граница защиты названа в докстроке — от случайности,
    не от умысла.
  - CONTEXT.md и комментарий `RETURN_TAIL_DAYS`: окно активности — от
    первого визита человека, общее на обе куки (спека это уже говорила,
    словарь отстал).
  - сторож предыстории сверял день −N с днём N, а сталкиваются −N и
    N−1; тем же классом слепоты страдали сторожа независимости состава
    и дня и различия компонентов — все три переписаны на сверку со всем
    куском адресов, куда подпоток мог бы попасть.
- Проверка:
  - make test (297), make lint, make typecheck;
  - батарея из 17 мутантов по plan/world/seeds — выживших нет; гоняется
    с PYTHONDONTWRITEBYTECODE=1: цикл правки и отката внутри одной
    секунды оставлял устаревший .pyc, и тесты шли по старому байт-коду.
- Отклонено с доводом:
  - перепроверка настаивала, что сторож неизменности не закрыт: через
    `.base` вида владелец данных размораживается. Верно фактически, но
    закрывающего состояния у находки нет — владелец памяти в numpy
    размораживается всегда, а копия когорты на каждый вызов меняет 2 мс
    на 16 МиБ копирования и убивает смысл запоминания.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-02 13:13:41 +03:00
co-authored by Claude Opus 5
parent 2502f906a8
commit d17bd4e876
5 changed files with 71 additions and 20 deletions
+13 -1
View File
@@ -64,11 +64,23 @@ class Cohort:
pair_order_days: NDArray[np.int64]
def __post_init__(self) -> None:
"""Когорта запоминается, поэтому массивы отдаются только на чтение."""
"""Когорта запоминается, поэтому массивы отдаются только на чтение.
Не флагом на самом массиве, а видом на замороженный: флаг
вызывающий снял бы и сам, а испорченную когорту получили бы потом
все дни окна. Вид данных не копирует — платы за это нет.
Граница у защиты честная: полной неприкосновенности numpy не даёт —
добравшись до владельца данных через `.base`, разморозить можно
что угодно. Это защита от случайной записи и короткого пути, но не
от умысла; умысел закрывался бы копией когорты на каждый вызов —
16 МиБ на день вместо двух миллисекунд.
"""
for field in fields(self):
value = getattr(self, field.name)
if isinstance(value, np.ndarray):
value.flags.writeable = False
object.__setattr__(self, field.name, value[...])
@property
def pairs(self) -> int:
+4 -3
View File
@@ -43,9 +43,10 @@ ONE_SHOT_PERCENT = 75
# притоке 3 800 (спека, разделы 5 и 9).
RETURN_COUNT_WEIGHTS = (25, 20, 15, 12, 9, 7, 5, 4, 2, 1)
# Хвост возвратов: окно активности куки от её первого визита. За краем окна
# кука не возвращается. Оно же — глубина предыстории: столько когорт живёт
# до D0, чтобы дневная аудитория была на полке с самого первого дня.
# Хвост возвратов: окно активности человека от его первого визита, общее на
# обе его куки. За краем окна кука не возвращается. Оно же — глубина
# предыстории: столько когорт живёт до D0, чтобы дневная аудитория была на
# полке с самого первого дня.
RETURN_TAIL_DAYS = 90
# Профиль возвратов по дням от первого визита: почти всё в первую неделю,