добавлен словарь категориальных значений HAE → коды HealthKit
- фазы сна, контекст пульса и имена тренировок попадают в реестр `category_value` (миграция 00010): строка хранится дословно, выведенный код лежит рядом отдельной записью, а не полем внутри точки - словарь и синонимы кодов живут в бинаре (`internal/healthkit`); локаль из `Accept-Language` сужает поиск, но в ключ реестра не входит — заголовков в сыром архиве нет - наблюдение входит в отпечаток витрины, выведенный код — нет: он производная от словаря, а не от журнала
This commit is contained in:
@@ -16,7 +16,9 @@ import (
|
||||
|
||||
"git.vakhrushev.me/av/healthlog/internal/catalog"
|
||||
"git.vakhrushev.me/av/healthlog/internal/hae"
|
||||
"git.vakhrushev.me/av/healthlog/internal/healthkit"
|
||||
"git.vakhrushev.me/av/healthlog/internal/ident"
|
||||
"git.vakhrushev.me/av/healthlog/internal/replay"
|
||||
"git.vakhrushev.me/av/healthlog/internal/store"
|
||||
)
|
||||
|
||||
@@ -155,6 +157,90 @@ func TestReplayЖивогоАрхива(t *testing.T) {
|
||||
coords, labels)
|
||||
}
|
||||
t.Logf("координат сна %d, различных меток %d", coords, labels)
|
||||
|
||||
measureCategories(t, dst, second)
|
||||
}
|
||||
|
||||
// measureCategories проверяет реестр категориальных значений на живом корпусе.
|
||||
//
|
||||
// Здесь он проверяется единственным способом, каким это вообще возможно: против
|
||||
// строк, которые телефон действительно присылал. Заголовков в архиве нет, то
|
||||
// есть локаль у всех доставок пуста — прогон заодно доказывает, что вывод кода
|
||||
// не зависит от уцелевшей учётной строки.
|
||||
//
|
||||
// Утверждаются СВОЙСТВА, а не числа: число строк реестра и число строк без кода
|
||||
// растут вместе с потоком, а прогон живого архива в гейт не входит — константа
|
||||
// покраснела бы молча (docs/review.md, 2026-08-02). Измеренное печатается.
|
||||
func measureCategories(t *testing.T, dst *store.Store, second replay.Report) {
|
||||
t.Helper()
|
||||
|
||||
values, err := dst.CategoryValues(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("реестр категориальных значений: %v", err)
|
||||
}
|
||||
if len(values) == 0 {
|
||||
t.Fatal("реестр пуст — категориальные значения не извлекаются вовсе")
|
||||
}
|
||||
if int64(len(values)) != second.Categories {
|
||||
t.Errorf("повторное проигрывание изменило число строк реестра: %d → %d",
|
||||
len(values), second.Categories)
|
||||
}
|
||||
|
||||
var withCode, withoutCode int
|
||||
for _, v := range values {
|
||||
if v.Code == "" {
|
||||
withoutCode++
|
||||
continue
|
||||
}
|
||||
withCode++
|
||||
// Всякий выведенный код обязан быть каноническим: обе формы имени,
|
||||
// которые Apple дала одному значению, сходятся в одну (находка 43).
|
||||
// Координаты — метрика и поле; сам код не печатается: он константа
|
||||
// бинаря, но напечатанный рядом с живым архивом сообщает, что эта фаза
|
||||
// у человека была.
|
||||
if healthkit.Canonical(v.Code) != v.Code {
|
||||
t.Errorf("%s/%s: выведенный код не канонический", v.Metric, v.Field)
|
||||
}
|
||||
// Провенанс — доставка журнала, а не пустое место.
|
||||
if v.FirstDeliveryID == "" || v.FirstSeen.IsZero() {
|
||||
t.Errorf("%s/%s: провенанс не заполнен", v.Metric, v.Field)
|
||||
}
|
||||
}
|
||||
|
||||
// Фазы сна — единственное, что словарь покрывает, и покрывать он их обязан:
|
||||
// ради этой сверки задача и существует. Строки берутся из ЖИВОГО архива, а
|
||||
// не из литерала теста.
|
||||
var sleepValues, sleepCoded int
|
||||
for _, v := range values {
|
||||
if v.Metric != "sleep_analysis" || v.Field != "value" {
|
||||
continue
|
||||
}
|
||||
sleepValues++
|
||||
if v.Code != "" {
|
||||
sleepCoded++
|
||||
}
|
||||
}
|
||||
if sleepValues == 0 {
|
||||
t.Fatal("фаз сна в реестре нет — проверять нечего")
|
||||
}
|
||||
if sleepCoded != sleepValues {
|
||||
// Печатается ЧИСЛО, а не перечень. Перечень был бы удобнее, и ровно
|
||||
// поэтому его тут быть не может: прогон идёт против рабочего архива, а
|
||||
// фаза сна — значение точки. `CLAUDE.md`, «Запреты»: ничего из `./data`
|
||||
// не попадает ни в логи выше `DEBUG`, ни в вывод агента. Ветка
|
||||
// срабатывает как раз тогда, когда телефон принёс НОВУЮ фазу, то есть
|
||||
// именно тогда, когда соблазн напечатать её сильнее всего.
|
||||
//
|
||||
// Адрес, по которому строки смотрят, отпечатывать не нужно: они лежат в
|
||||
// пересобранной базе, `SELECT value FROM category_value WHERE code = ''`.
|
||||
t.Errorf("фаз сна без кода %d из %d — словарь неполон; строки смотреть в реестре пересобранной базы",
|
||||
sleepValues-sleepCoded, sleepValues)
|
||||
}
|
||||
|
||||
// Числа печатаются: они растут с корпусом и утверждению не подлежат.
|
||||
// Значения при этом НЕ печатаются — это данные о здоровье.
|
||||
t.Logf("реестр: строк %d, с кодом %d, без кода %d; фаз сна %d, все с кодом",
|
||||
len(values), withCode, withoutCode, sleepValues)
|
||||
}
|
||||
|
||||
// measureStyles прогоняет измерение рода агрегации на витрине, собранной из
|
||||
|
||||
Reference in New Issue
Block a user