# Задачи

## 1. Словарь HealthKit — `internal/healthkit`

- [x] 1.1 Новый пакет без внутренних зависимостей: словарь `(локаль, строка) → код`
      с выведенными фазами сна (находка 43) и **плоская** таблица синонимов кодов
      (`…Asleep` → `…AsleepUnspecified`).
- [x] 1.2 `Canonical(код)` — одно чтение карты, без обхода цепочек и ограничения
      глубины: плоскость таблицы держит тест, а не рантайм.
- [x] 1.3 `Code(локаль, строка)` — три разряда: точная пара, однозначность по
      всем локалям, пустой код.
- [x] 1.4 Нормализация локали по lookup RFC 4647: первый тег, подтеги отсечены,
      регистр свёрнут.
- [x] 1.5 Тесты: обе формы синонима сходятся; таблица плоская (ни одно значение
      не является ключом); неизвестная строка — пустой код; табличный тест
      нормализации (`ru`, `RU`, `ru-RU`, `RU-ru,ru;q=0.9`, ` ru `, `*`, пусто,
      мусор, сто тегов).

## 2. Разбор — `internal/hae`

- [x] 2.1 `Meta.Locale`; объявленный список категориальных полей
      (`sleep_analysis.value`, `heart_rate.context`, `workouts.name`).
- [x] 2.2 Извлечение значений тем же разбором заголовка точки; поля —
      `json.RawMessage`, принимается только непустая JSON-строка.
- [x] 2.3 Имя тренировки — из уже разобранного заголовка сущности. `softString`
      превращает значение не того типа в `""`, а пустая строка наблюдением не
      считается: «имени не было» и «имя приехало числом» дают один исход, и он
      верный. Второго разбора мегабайтного маршрута не заводим.
- [x] 2.4 `Result.Categoricals` — детерминированное множество наблюдений
      `(метрика, поле, строка, код)`, ключ тот же, что у реестра; счётчики
      `CategoricalUnknown` и `CategoricalDropped`.
- [x] 2.5 Границы: `maxCategoricalValues = 64`, `maxCategoricalValueLen = 128`;
      длинное отбрасывается, не обрезается; уцелевший набор — функция множества
      (сортировка по ключу, затем усечение).
- [x] 2.6 Тесты на реальных пакетах `testdata` (`sparse_sleep.json`,
      `handmade_edge.json`, `workout_route.json`, `raw.json`): строка берётся
      **из пакета**, а не из литерала теста; побайтное равенство `Point.Raw`
      фрагменту тела; обе границы; порядок точек на результат не влияет.

## 3. Хранилище — `internal/store`

- [x] 3.1 Миграция `00010_category_value.sql` — таблица `category_value`,
      ключ `(metric, field, value)`, `WITHOUT ROWID`, с обоснованием в
      комментарии.
- [x] 3.2 Upsert внутри транзакции `Merge`: код обновляется всегда, провенанс —
      минимум по `(received_at, id)`.
- [x] 3.3 `DeliveryForParse` отдаёт заголовки доставки.
- [x] 3.4 Раздел реестра в `Fingerprint` (признак `c`, поля с длиной впереди);
      **`code` в раздел не входит**.
- [x] 3.5 Чтение реестра и его счёт — для отчёта пересборки и тестов.
- [x] 3.6 Тесты: идемпотентность повторной свёртки, минимум провенанса, шесть
      порядков трёх доставок дают один реестр, влияние наблюдения на отпечаток,
      независимость отпечатка от кода, пустой реестр, значение с разделителем и
      нулевым байтом.

## 4. Свёртка — `internal/fold`

- [x] 4.1 Локаль из `Accept-Language` заголовков доставки.
- [x] 4.2 Счётчики в `Stats` и в единственном логирующем чекпоинте; строк и
      кодов в логе нет.
- [x] 4.3 Тест на отсутствие строк и кодов в логе — разбор записи, а не поиск в
      сыром буфере.

## 5. Пересборка — `cmd/healthlog`

- [x] 5.1 Счётчик строк реестра «до и после» в отчёте `reindex`.
- [x] 5.2 Ожидаемый класс расхождения «появилась единица хранения».
- [x] 5.3 Тест отчёта: пустой реестр рабочей витрины против непустого
      пересобранной.

## 6. Документы

- [x] 6.1 `docs/database.md` — таблица `category_value` (без правки гейт красный).
- [x] 6.2 `docs/architecture.md` — раздел «Категориальные значения» приводится в
      соответствие с принятой формой хранения, тем же коммитом, что и миграция.

## 7. Оракулы

- [x] 7.1 `task gate` зелёный.
- [x] 7.2 `task verify:archive` на живом архиве основного репозитория
      (`ARCHIVE=…/data/raw`) — правило разбора изменено. Новые утверждения
      проверяют **свойства**, а не числа, производные от размера корпуса
      (число строк реестра и счётчик строк без кода растут с потоком); числа
      печатаются `t.Logf`.
- [x] 7.3 `task verify:busy` — свёртка трогает транзакцию слияния.

## Приёмочные критерии

### Из постановки задачи

- [x] фаза сна из потока («БДГ») и из экспорта Apple
      (`HKCategoryValueSleepAnalysisAsleepREM`) за один период сопоставляются
      напрямую — оракул: запрос на живой базе за период с известным перекрытием,
      ноль несопоставимых строк.
      **Оракул недостижим в рамках этой задачи:** импорт родного экспорта Apple
      объявлен Non-Goal, кодов экспорта в живой базе нет и не появится. Снимается
      настолько, насколько возможно: все шесть измеренных строк фаз сна
      (находка 43), прочитанные из живого архива, дают непустой канонический код,
      и множество выведенных кодов совпадает с множеством кодов сна из находки 43.
      Полная сверка принадлежит задаче `apple-export-import`; расхождение названо
      в отчёте, а не обойдено.
- [x] строка сохранена **дословно**, код лежит рядом отдельным полем — оракул:
      тест разбора на реальном пакете HAE из `internal/hae/testdata`
- [x] незнакомая строка даёт пустой код, разбор не падает, а событие попадает в
      счётчик — оракул: тест на выдуманной фазе сна плюс проверка счётчика
- [x] переименование кода самой Apple (`…Asleep` → `…AsleepUnspecified`,
      находка 43) не раскалывает историю — оракул: тест на паре синонимов, обе
      формы сходятся в один код
- [x] повторный прогон живого архива даёт то же состояние — оракул:
      `task verify:archive`

### Из ревью предложения (рубрика профиля `design`)

- [x] **Дословность и живучесть точки.** `Point.Raw`/`Entity.Raw` побайтно равны
      фрагменту тела; значение не-строка, отсутствие поля, пустая строка,
      невалидный UTF-8 — точка разбирается как обычно, наблюдения не выводится.
      *Оракул:* тесты на четырёх пакетах `testdata` + табличный тест форм.
- [x] **Реестр — функция префикса журнала, а не порядка свёртки.** Три доставки
      во всех шести порядках дают один реестр и один отпечаток; повторная
      свёртка не меняет ни одной колонки. *Оракул:* тест перестановок + тест
      повтора.
- [x] **Уцелевший при переполнении набор — функция множества.** *Оракул:* два
      тела с одним множеством строк и разным порядком точек дают один результат.
- [x] **Отпечаток инъективен по наблюдению и слеп к коду.** Различие в значении,
      ключе, числе строк — меняет отпечаток; различие только в коде — не меняет;
      значение с разделителем и цифрами границу поля не подделывает. *Оракул:*
      тест с враждебной строкой + тест «пустой реестр».
- [x] **Расхождение «появилась единица хранения» отличимо машиной.** *Оракул:*
      тест отчёта `reindex`: класс назван, счётчик напечатан, прогон не красный.
- [x] **Ключ восстановим из того, что переживает доставку.** Локали в ключе нет;
      доставка без `Accept-Language` даёт ту же строку реестра. *Оракул:* тест
      без заголовка + `task verify:archive` (архив заголовков не несёт).
- [x] **Нормализация локали не расщепляет язык вариантами тега.** *Оракул:*
      табличный тест на девяти входах, включая `RU` и `*`.
- [x] **Словарь опознаёт байты живого потока, а не литерал теста.** *Оракул:*
      приёмочный тест берёт строку из пакета `testdata`.
- [x] **Словарь и синонимы — чистая функция без изменяемого состояния.**
      *Оракул:* тесты по таблицам целиком + `go test -race` (шаг гейта).
- [x] **Границы названы числом, отброшенное считается, рост реестра назван.**
      *Оракул:* тесты обеих границ + число строк реестра живого архива в отчёте
      задачи.
- [x] **Утверждения на живом корпусе — свойства, не числа от размера корпуса.**
      *Оракул:* `task verify:archive` дважды подряд + чтение утверждений.
- [x] **Ни одна строка и ни один код не выходят выше `DEBUG`.** *Оракул:* тест
      по разобранной записи лога; `task verify:busy`.
