Эволюция продукта
Техническое описание системы для тех, кому предстоит с ней работать: модель данных, процессы, архитектура, контракты обмена, развёртывание и — отдельно — ограничения.
Часть I. Введение и обзор
1. О системе#
1.1. Какую задачу решает#
«Племенная книга» — информационная система Ассоциации производителей КРС голштинской породы. Она ведёт учёт племенных животных, хранит происхождение и продуктивность, считает племенную ценность, выпускает племенные документы и — это главное — отвечает на вопрос, кто ручается за каждую цифру.
Ценность племенного животного подтверждается данными о предках и потомстве. Сегодня эти данные разбросаны: часть в системе управления стадом, часть в бумажных свидетельствах, часть в таблицах зоотехника. Покупатель, глядя на цифру продуктивности, не знает её происхождения — сама ферма её ввела или она подтверждена независимой стороной. Система устроена вокруг этой разницы: у каждой записи есть уровень достоверности и история изменений.
1.2. Кому адресован этот документ#
Прежде всего техническим специалистам, которым предстоит решить одну из двух задач: построить собственную интеграцию со своей стороны или оценить Ассоциацию как технологического партнёра. Это не только хозяйства — это лаборатории генотипирования, поставщики систем управления стадом, сервисные организации по воспроизводству, разработчики отраслевых сервисов.
Документ отвечает на четыре практических вопроса: какая модель данных вас ждёт, какие контракты обмена уже есть и какие спроектированы, что нужно для развёртывания собственного контура, и — отдельным приложением — чего в системе нет. Последнее не менее важно: интеграция ломается о недосказанное, а не о недостающее.
Отметки у разделов читаются так: работает — есть в коде и работает; спроектировано — контракт продуман и записан, реализации нет; план — направление известно, сроки нет.
1.3. Принципы#
- Владелец распоряжается своими данными. Публичность записи включает хозяйство, а не Ассоциация. Ассоциация ручается за данные перед другими, но не открывает их за владельца.
- У цифры есть происхождение. Уровень достоверности, автор, время правки и предыдущее значение хранятся вместе с записью, а не выводятся из журналов постфактум.
- Правила предметной области живут в базе. Возраст, доли, знаки, запрет быть себе родителем — ограничения, которые нельзя обойти ни импортом, ни служебным скриптом.
- Прошлое не редактируется. Документы отзываются, а не удаляются; правки дописываются в журнал; типы событий выводятся из обращения, а не стираются.
- Стандарт вместо собственного формата. Обмен проектируется на ICAR ADE, документы — по форме Регламента (ЕС) 2016/1012. Свой формат пришлось бы объяснять каждому партнёру отдельно.
- Отказ объясняется построчно. Импорт называет номер строки, идентификатор и причину. «Часть данных не принята» без указания какой — не сообщение, а тупик.
1.4. Текущее состояние и что это значит#
Версия 0.16.0-alpha. Работающий прототип: интерфейс, модель данных и расчёты действуют на объёме 280 тысяч животных и 42 хозяйств, но данные синтетические — построены по реальным распределениям, и ни одно хозяйство пока не ведёт здесь настоящий учёт.
Для интегратора это значит следующее. Модель данных и контракты чтения можно изучать и на них ориентироваться, но обещания обратной совместимости нет: ноль в начале номера версии именно об этом. Автоматической сборки, тестов и мониторинга нет — служебные ревизии запускаются вручную. Полный список ограничений — в приложении Б.
2. Быстрый старт#
2.1. Требования к окружению#
Node.js 22.12 и новее, PostgreSQL 16, около 2 ГБ свободной памяти на сборку. Внешних сервисов при локальном запуске не требуется: почта, хранилище файлов и очереди в текущей версии не используются.
| Слой | Что используется |
|---|---|
| Приложение | Next.js 16.3 — App Router, серверные компоненты и действия |
| Данные и админка | Payload CMS 3.85 поверх PostgreSQL через Drizzle |
| Интерфейс | React 19.2, Tailwind CSS 4 |
| Сборка | Turbopack; выпуск в режиме standalone |
2.2. Запуск#
npm install
cp .env.example .env # DATABASE_URI и PAYLOAD_SECRET
npm run db:sync # схема и миграции в согласованное состояние
SEED_CONFIRM=1 npm run seed # демонстрационные данные
npm run dev # http://localhost:3000Переменная SEED_CONFIRM — предохранитель, а не формальность. Наполнение базы удаляет существующие записи, и защита от запуска этой команды «по привычке» на боевом контуре важнее удобства.
Отдельно о npm run db:sync: он приводит в порядок частый случай, когда схема в базе уже изменена разработкой, а журнал миграций об этом не знает. Разбор — в главе 15.
2.3. Роли#
| Роль | Что видит и что может |
|---|---|
farm — хозяйство | своё стадо целиком, публичные записи чужих; ведёт данные, управляет публичностью, отвечает на запросы доступа, подаёт заявки на верификацию |
expert — эксперт Ассоциации | читает данные всех хозяйств, разбирает пакеты и заявки, оставляет замечания; править чужие данные не может |
admin — администратор Ассоциации | всё перечисленное плюс справочники НСИ, членство и удаление записей |
| аноним | публичный список и открытые карточки; может запросить доступ |
Проверки собраны в одном месте — src/access/index.ts. Разделение чтения и записи сделано намеренно: эксперту нужно видеть всё, чтобы проверять, и не нужно ничего менять — иначе исчезает смысл независимой проверки.
Часть II. Модель данных и предметная логика
3. Сущности и связи#
3.1. Граф сущностей#
В центре — животное. Всё остальное либо описывает его состояние в момент времени, либо связывает с организацией, документом или другим животным.
| Группа | Коллекции | Что хранит |
|---|---|---|
| Ядро | animals, organizations, herds | животное, хозяйство, стадо |
| Фенотип | milk-tests, calvings, inseminations, health-events, animal-exteriors | контрольные дойки, отёлы, осеменения, здоровье, оценка экстерьера |
| Оценка | animal-evaluations, index-values, index-bases, index-profiles | история оценок, рассчитанный индекс, база сравнения, профили весов |
| Оборот данных | data-submissions, animal-revisions, verification-requests, access-requests | пакеты загрузки, журнал правок, заявки на верификацию и на доступ |
| Документы и НСИ | documents, events, media, справочники | выданные документы, лента событий, файлы, породы и линии |
3.2. Животное и его идентификаторы#
Карточка разбита на смысловые блоки: идентификация, происхождение, фенотип и продуктивность, генетика, движение, оценка. Идентификаторов несколько, и они не взаимозаменяемы — это первое, обо что спотыкается интеграция.
| Поле | Что это |
|---|---|
uuid | GUID записи. Присваивается при создании, не меняется никогда, не зависит от номеров хозяйства. Устойчивый ключ сопоставления при обмене |
identNumber | индивидуальный номер, основной для человека; формат проверяется правилами ID_RULES |
idFormat | какому правилу подчинён номер: rf, icar, usa, can, deu, internal |
altIds.isoId | номер средства маркирования по ISO 11784/11785 |
altIds.internationalId | международный номер для обмена оценками (Interbull) |
altIds.earTag | номер ушной бирки — видимая метка на животном |
altIds.gpkMark | марка и номер тома государственной племенной книги |
Часть полей вычисляется хуками записи, а не приходит извне: транслитерация клички по ГОСТ 7.79-2000, сумма жира и белка, служебные ранги сортировки, автор и время последней правки, флаг инбридинга выше 25%. Коэффициент инбридинга при записи не пересчитывается — он считается на лету при разборе родословной и при выпуске документа.
3.3. Фенотип и продуктивность#
Первичные наблюдения и сводка по ним разведены. Контрольные дойки, отёлы, осеменения и события здоровья лежат отдельными записями со своими датами; в карточке животного хранится сводка (summary) и разбор по лактациям. Смешивать их нельзя: сводка пересчитываема, наблюдение — нет.
Оценка племенной ценности хранится ещё в одном измерении — история оценок отдельно от текущего снимка. Это нужно документам: свидетельство ссылается на значения на момент выдачи, и пересчёт индекса не должен менять уже выданную бумагу задним числом.
3.4. Служебные ранги — важно при чтении через API#
Сортировка sort=-ipc формально работает, но первыми придут животные без оценки: PostgreSQL при ORDER BY … DESC ставит NULL в начало. Само приложение сортирует по -ipcRank и -summary.milkRank, где пустое значение заменено на −1 000 000. Внешнему клиенту, которому нужно «сначала лучшие», следует использовать те же поля.
Второе неочевидное место — архив. Служебные записи предков (archived = true) исключаются только на страницах приложения; REST и GraphQL возвращают их наравне с остальными. Условие приходится задавать явно.
4. Достоверность и происхождение данных#
4.1. Уровни достоверности#
У каждой записи есть уровень (trustLevel, от −1 до 3). Он не украшение интерфейса: от него зависит, можно ли выпустить племенной документ и попадёт ли животное в общую книгу.
| Уровень | Кто ручается | Чем именно |
|---|---|---|
| −1 · Отклонено | эксперт Ассоциации | нашёл расхождение, документы не выпускаются |
| 0 · Черновик | никто | внесено, но не заявлено готовым |
| 1 · Заявлено хозяйством | хозяйство | согласилось с разбором пакета и разрешило показ |
| 2 · Подтверждено лабораторией | лаборатория | в книге лежит её протокол по этому животному |
| 3 · Верифицировано ассоциацией | эксперт Ассоциации | разобрал записи по заявке на верификацию |
Поле закрыто на прямую запись всем без исключения: до этого руководитель хозяйства мог поставить себе третий уровень обычным запросом к API. Первую ступень даёт публикация пакета — это заявление хозяйства о собственных данных, и подписью Ассоциации она не притворяется. Вторая выводится из протокола лаборатории и снимается, если протокол отозвали. Третью ставит только разбор заявки на верификацию.
Третья ступень означает две вещи сразу, и вторую до недавнего времени не проверяло ничто: «расхождений нет» и «передано всё необходимое». Запись без единого отёла и без единой дойки не противоречит ничему — противоречить нечему, — и знак Ассоциации вставал на пустую карточку. Теперь заявку держат два заслона: находки проверок (verification-gate.ts) и полнота (completeness.ts).
| Требуется | Зачем |
|---|---|
| дата рождения | от неё считаются возраст первого отёла, номера лактаций и проверки родословной |
| порода | без породы запись не сравнить со сверстницами и не включить в отчёты |
| отец: запись или номер | книга подтверждает происхождение; без отца подтверждать нечего |
| мать: запись или номер | без матери рвётся материнская линия и не считается инбридинг потомка |
| хотя бы один отёл | корова без отёла — это либо не корова, либо потерянная история |
| контрольные дойки (минимум 6, если заявлен удой за 305 дней) | удой за 305 дней без замеров — число, взятое неизвестно откуда |
Состав назван хозяйству до подачи — на странице «Стадо → Верификация», — и берётся там из того же правила. Требование, о котором узнают из отказа, читается как придирка; названное заранее — как условие.
4.2. Журнал правок#
Каждая ручная правка карточки пишется в animal-revisions: поле, было, стало, кто и когда. Описано около пятидесяти полей — для связей сохраняется не идентификатор, а название на момент правки, иначе через год журнал превращается в столбец чисел.
Импорт и служебные скрипты журнал не засоряют: у хука есть признак context.skipJournal. Смысл журнала — ответить на вопрос «дата рождения такой была или её поправили», а массовая загрузка на него не отвечает.
4.3. Пакеты загрузки#
Данные приходят не записями, а пакетом: data-submissions хранит файл, разбор, историю состояний, назначенного эксперта и список замечаний. Замечание указывает на конкретное животное и имеет степень — от «обратить внимание» до «исправить», последняя исключает запись из приёмки.
Важное отличие текущей логики от спроектированного приёма по ADE: сейчас пакет принимается или отклоняется целиком, а в контракте ADE предусмотрен частичный приём — непрошедшие строки отклоняются поштучно.
5. Видимость и доступ#
5.1. Две ступени публичности#
У видимости животного два независимых переключателя, и путать их нельзя. Оба выключены по умолчанию: новое животное не появляется в книге, пока владелец этого не захочет.
| Поле | Что открывает |
|---|---|
publicVisible | строку списка: номер, кличка, владелец, пол, возрастная группа, состояние, удой, жир, белок, ИПЦ |
publicDetails | карточку целиком: оценку, экстерьер, фенотип, происхождение, события, документы |
Проверяются они по очереди: сначала правило чтения коллекции решает, отдавать ли запись вообще, и только потом снимается или не снимается замок с подробностей. Отсюда следствие: пока publicVisible выключен, значение publicDetails ни на что не влияет — постороннему записи не существует, по прямой ссылке он получит 404.
5.2. Чужая карточка выглядит иначе#
Открытая карточка чужого хозяйства оформляется другим фоном и другой шапкой с надписью «Чужое хозяйство». Причина практическая: зоотехник открывает карточки вперемешку, и при одинаковом виде чужие данные принимают за свои и пытаются править. Гостю пишется «Открытые данные», а не «Чужое хозяйство»: сравнивать не с чем — он вполне может быть сотрудником этого самого хозяйства.
5.3. Запрос доступа#
Увидев закрытую запись, посторонний отправляет запрос с целью и текстом. Решение принимает владелец, а не Ассоциация: Ассоциация ручается за данные, но не распоряжается ими. Ответ приходит в ленту уведомлений заявителя.
Известное ограничение: одобрение открывает карточку целиком и бессрочно. Это грубо в обе стороны — покупателю нужны происхождение и продуктивность одного животного, а хозяйство, не имея среднего варианта, отказывает.
5.4. Точечный доступ#
Работает. Доступ с границами по трём измерениям: к чему — вся карточка, только происхождение, только продуктивность или только оценка; на какой срок; с правом отозвать в любой момент. Обращения пишутся в журнал (access-views): хозяйство должно видеть, что доступ используется по назначению, иначе оно перестанет его выдавать.
Область гранта уважают не только карточка, но и все связанные коллекции: открыв «только происхождение», доступа к контрольным дойкам не получают ни через страницу, ни через API. Это одно правило — scopedRead в src/access/index.ts, — и за ним следит ревизия npm run audit:grants.
Тому, у кого нет учётной записи, выдаётся ссылка на просмотр (share-links, страница /share/[token]) — с тем же сроком и той же областью.
Техническая сложность здесь была в том, что проверка «есть ли действующий грант» попадает на горячий путь — страницу книги и карточку. Условие на связанную таблицу превращается в соединение, и на 280 тысячах записей это стоило секунд. Отсюда переключатель PLEMKNIGA_GRANTS_OFF: он снимает соединение целиком, если однажды окажется, что цена выросла.
6. Целостность#
6.1. Ограничения в базе#
Тридцать проверок предметной области вынесены в саму базу: диапазоны дат и возраста, неотрицательность удоя, доли в пределах ноль—единица, процентиль от 0 до 100, запрет животному быть собственным родителем. Приложение проверяет то же самое раньше и с внятным сообщением, но последний рубеж — база: её не обойдёт ни импорт, ни разовый скрипт, ни ошибка в новом коде.
Список собран в src/lib/db-constraints.ts и сверяется ревизией. Для интегратора это значит, что данные, пришедшие извне, будут отклонены на тех же условиях, что и введённые руками, — отдельного «мягкого режима» для API нет.
6.2. Архив предков#
Разбор родословной вглубь заводит записи предков, которых нет ни в одном хозяйстве: они нужны для расчёта инбридинга, но не являются частью стада. Такие записи помечаются archived и исключаются из списков приложения, оставаясь доступными по прямой ссылке и в дереве происхождения.
7. Глоссарий: что путают чаще всего#
Шесть пар терминов, которые звучат похоже и означают разное. Цена ошибки здесь высокая — от неверно прочитанного отчёта до неверно выпущенного документа.
7.1. Пары, которые нужно развести#
| Одно | Другое | Чем различаются |
|---|---|---|
Уровень достоверности записи trustLevel, −1…3 | Достоверность оценки reliabilityLevel, 1…5 | первое — кем проверены сами данные, второе — насколько надёжен прогноз племенной ценности; шкалы и источники разные, меняются независимо |
| Достоверность оценки, 1…5 | R, % | первое — ступень готовности оценки для человека, второе — статистическая величина: доля дисперсии истинной племенной ценности, объяснённая оценкой |
ИПЦ ipc | ПИ production.productionIndex | ИПЦ — свод по всем группам признаков; ПИ — свод только по продуктивным, одна строка внутри блока продуктивности |
Прогноз forecast | Факт summary, lactations[] | прогноз — наследуемая часть, которую животное передаёт потомству; факт — то, что животное надоило само |
| Инбридинг животного (COI) | Коэффициент родства | COI — свойство одного животного; коэффициент родства — свойство пары, и он вдвое больше, чем COI их будущего потомка |
Линия line | Семейство family | линия ведётся по отцам от родоначальника-быка, семейство — по матерям; поля разные, справочник за ними один |
Часть III. Бизнес-процессы и сквозные сценарии
8. Жизненный цикл данных#
8.1. Поступление#
Три пути, и они не равнозначны по назначению:
- Импорт CSV — основной для регулярного потока. Разбирает файл, заводит пакет, обновляет существующих животных по индивидуальному номеру и объясняет каждую непринятую строку.
- Ручной ввод — для того, что файлом не приходит: купленное животное, расхождение с бумажным свидетельством, событие по ходу дела. Пишется в журнал правок.
- API —
POST /api/animalsи остальные коллекции. Работают те же серверные проверки, что и в интерфейсе.
8.2. Проверка#
Пакет попадает в очередь Ассоциации с возрастом ожидания — сколько дней он ждёт разбора. Эксперт открывает пакет и видит две вещи сразу: содержимое и результат автоматического поиска противоречий. Пятьдесят три правила ищут несовместимые даты, невозможные величины, разрывы в происхождении, значения за пределами правдоподобного диапазона.
Автоматика ничего не решает — она сокращает то, что эксперт должен просмотреть глазами. Решение остаётся за человеком и оформляется замечаниями по конкретным записям, а не общей резолюцией по пакету.
8.3. Публикация#
После разбора пакет публикуется: уровень достоверности вошедших животных поднимается. Показывать ли животных в общей книге — отдельное решение владельца, и оно от проверки не зависит. Ассоциация ручается за данные; открывает их хозяйство.
9. Процессы Ассоциации#
9.1. Верификация хозяйства#
Хозяйство подаёт заявку с целью — доверие, документ или членство — и списком животных. Заявка получает номер вида В-2026-001 и проходит состояния «новая → на проверке → одобрена / отклонена». Замечание со степенью «исправить» исключает конкретное животное из одобрения, не блокируя остальные.
9.2. Членство#
Членство решает две вещи: показывать ли животных хозяйства в общей книге и принимать ли от него заявки на верификацию. Собственные данные хозяйство ведёт независимо от решения — оно их владелец. Состояние членства может быть приостановлено, и это работает сразу.
9.3. Выпуск документов#
Племенное свидетельство и зоотехнический сертификат выпускаются по форме Регламента (ЕС) 2016/1012. Выпуск требует уровня достоверности «верифицировано», полной готовности карточки и отсутствия действующего документа того же вида. Номера сквозные: ПС-2026-0001, ЗС-2026-0001.
Третье условие — не педантизм: два непогашенных свидетельства на одно животное это ровно тот случай, когда в спорной ситуации предъявляют то, которое выгоднее.
Выданный документ не удаляется — он отзывается с обязательным указанием причины, автора и времени, и запись об этом остаётся в журнале выдачи навсегда. Документ, который можно стереть, ничего не подтверждает.
9.4. Качество книги#
Сводка по всей книге: полнота происхождения, доля подтверждённых записей, противоречия в данных. Каждый показатель ведёт в список конкретных записей — цифра без возможности перейти к причине бесполезна. Расчёт сделан тремя независимыми запросами с ограничением по времени: медленный показатель не должен ронять всю страницу. 660 мс на 280 тысячах записей.
10. Перенос данных#
10.1. Импорт и экспорт#
Импорт CSV сопоставляет строки с существующими животными по индивидуальному номеру: совпало — обновление, не совпало — создание. Каждая непринятая строка попадает в протокол с номером, идентификатором и причиной.
Выгрузка — GET /account/export?format=xlsx|csv|txt|xml|json, до 20 000 записей своей организации. Ограничение осознанное: выгрузка на 280 тысяч строк держит соединение минутами и всё равно заканчивается таймаутом посредника.
10.2. Перенос между контурами#
Порядок для нового кода всегда один: git push → деплой или payload migrate вручную → наполнение данными. Обратный порядок даёт ошибку вида column animals.for_sale does not exist — код знает про поля последнего коммита, база знает только про применённые миграции.
payload migrate идемпотентна: применённые пропускаются, применяются только новые.
Часть IV. Техническая архитектура и реализация
11. Технологическая основа#
11.1. Стек и почему он такой#
Payload CMS даёт из описания коллекций сразу три вещи: схему PostgreSQL, административный интерфейс и REST с GraphQL с теми же правами доступа, что и в приложении. Для системы, где предметная модель большая, а бюджет на инфраструктуру маленький, это решающий фактор — правила доступа пишутся один раз и действуют везде.
Next.js используется серверными компонентами: страницы ходят в базу напрямую через локальный интерфейс Payload, без обращения к собственному HTTP-слою. Формы работают через серверные действия. Клиентского JavaScript в приложении мало по замыслу, а не по недосмотру.
11.2. Слои#
| Каталог | Ответственность |
|---|---|
src/collections | предметная модель, хуки записи, права |
src/access | все правила доступа в одном месте |
src/lib | расчёты и запросы: родословная, индекс, проверки |
src/actions | серверные действия форм |
src/app/(frontend) | страницы |
src/scripts | служебные ревизии и обслуживание базы |
src/migrations | миграции схемы |
Расчёты вынесены в src/lib отдельно от страниц и от коллекций намеренно: их нужно вызывать и из интерфейса, и из скриптов проверки на живой базе, а проверка, повторяющая логику вместо того, чтобы вызывать её же, проверяет саму себя.
11.3. Состояние в адресной строке#
Отбор, сортировка, страница и открытая вкладка живут в параметрах адреса, а не в состоянии компонента. Практическое следствие: любой экран системы можно переслать ссылкой, и получатель увидит ровно то же. Для отчётов и переписки между хозяйством и Ассоциацией это оказалось важнее, чем плавность переключений.
12. Модули и алгоритмы#
12.1. Родословная и инбридинг#
Разбор дерева до девятого колена. Коэффициент инбридинга считается по формуле Райта с учётом инбридинга самих общих предков — упрощённый вариант без этой поправки на глубоких деревьях заметно занижает результат. Дополнительно считается доля крови и вклад каждого ключевого предка, а источники инбридинга размечаются: видно не только число, но и через кого оно возникло.
Расчёт выполняется на лету (analyzeAncestry) при открытии вкладки происхождения и при выпуске документа. Значение поля inbreeding в карточке — это то, что пришло с данными, и оно может расходиться с расчётом по дереву; флаг «требует согласования» ставится по полю, а не по расчёту.
12.2. Индекс племенной ценности#
Признаки приводятся к стандартизованным значениям по собственной базе сравнения, взвешиваются, корректируются на достоверность и переводятся в процентиль внутри группы сверстников. Веса задаются двумя способами: экономически (рубли на единицу признака) или селекционно (проценты влияния).
Рассчитанное хранится: индекс лежит строкой в index-values вместе с процентилем. Считать при каждом открытии страницы книги на 280 тысячах записей невозможно — это проверено на практике, а не предположено.
12.3. Профили весов и коррелированный отклик#
Профили — то, ради чего расчёт сделан настраиваемым. Одному хозяйству важнее белок, другому продуктивное долголетие; рейтинг перестраивается под экономику хозяйства, оставаясь сопоставимым между хозяйствами за счёт общей базы сравнения.
Отдельно считается коррелированный отклик: что произойдёт с остальными признаками при отборе по выбранному. Без него профиль весов — способ выстрелить себе в ногу: усиление одного признака тянет за собой другие, и не всегда в нужную сторону.
12.4. Автоматические проверки данных#
Пятьдесят три правила, собранные реестром src/lib/checks-registry.ts: согласованность дат рождения и отёлов, правдоподобность величин (удой 500…25 000 кг за лактацию и подобные границы), разрывы и противоречия в происхождении, несовместимые состояния. Правила намеренно отделены от ограничений базы: ограничение запрещает невозможное, правило указывает на подозрительное — второе не должно блокировать запись, но должно попадать на глаза эксперту.
12.5. Геномный конвейер#
Из геномного конвейера в коде есть две вещи, и обе не про маркеры: расчёт инбридинга по родословной и механика пакетной загрузки с протоколом ошибок. Тип загрузки genomics в перечне заведён, обработчика для него нет.
Не реализовано ничего из работы с самими маркерами: приём файлов генотипирования, нормализация аллелей, контроль качества, импутация, матрицы родства, ssGBLUP. Это сказано прямо, потому что «геномная оценка» в описании системы обычно означает совсем другое.
Одна особенность хранения, которую стоит знать заранее: у продуктивных признаков и признаков здоровья есть пара «прогноз + достоверность», у ИПЦ к ней добавлен процентиль, а признаки экстерьера хранятся одиночными числами без прогноза и без R. Когда оценка начнёт считаться внутри системы, экстерьеру понадобится та же пара — иначе достоверность экстерьерной части индекса негде будет показать.
13. API и интеграции#
13.1. REST и GraphQL#
Payload поднимает REST и GraphQL поверх модели автоматически. Аутентификация — POST /api/users/login, дальше cookie или заголовок Authorization: JWT ….
GET /api/animals?where[kind][equals]=bull
&where[ipc][greater_than]=1000
&sort=-ipcRank&limit=25&page=1&depth=1Поддерживаются операторы equals, not_equals, greater_than, less_than, in, contains, exists, логические and и or, а также depth — глубина разворачивания связей. Ответ содержит docs, totalDocs, page, totalPages, hasNextPage.
13.2. Права на чтение#
Правило одно и то же на всех уровнях: видно то, что видно у животного. Запись фенотипа или события не имеет собственной видимости — она наследует её у карточки, к которой относится. Так устроено scopedRead в src/access/index.ts: правило повторяет условие животного через связь и заодно учитывает области выданных доступов.
| Коллекция | Правило чтения |
|---|---|
animals | аноним — только публичные; пользователь — своя организация плюс публичные плюс открытые ему точечно; администратор — всё |
organizations, herds, справочники | читает кто угодно, включая анонима |
milk-tests, calvings, inseminations, health-events, events | то же, что у животного: область «продуктивность» |
animal-evaluations, animal-exteriors | то же, что у животного: область «оценка» |
documents | владелец, Ассоциация, тот, кому открыт доступ |
media | публичный файл — кто угодно; остальные — владелец и Ассоциация |
access-requests | заявитель, владелец животного, администратор |
users | сам себя либо администратор |
До версии 0.13 здесь была дыра, и этот раздел о ней предупреждал: ограничение по организации стояло только у животных, а через /api/milk-tests авторизованный получал первичные данные чужого хозяйства. Предупреждение снято не потому, что стало неудобным, а потому что дыра закрыта: правила переписаны, и за ними следят ревизии npm run check:security и npm run audit:tenancy — обе ходят от лица настоящего пользователя и пробуют достать чужое.
13.3. Собственные маршруты#
| Маршрут | Назначение |
|---|---|
GET /animals/:id/certificate/:kind | печатная форма: pedigree — племенное свидетельство, zootechnical — зоотехнический сертификат |
GET /account/export?format=xlsx|csv|txt|xml|json | выгрузка своего стада, до 20 000 записей: XLSX, CSV, TXT, XML, JSON |
GET /healthz | состояние базы и окружения, всегда HTTP 200 |
GET /healthz/live | проба живости для контейнера, базы не касается |
13.4. Обмен в форме ICAR ADE#
Целевой контракт интеграционного слоя — открытый стандарт ICAR ADE (OpenAPI 3.1, JSON Schema 2020-12). Он уже реализован рядом вендоров, и Section 15 Guidelines прямо отсылает к нему; собственный формат пришлось бы объяснять каждому партнёру отдельно.
POST /api/ade/milk-recording результаты контрольной дойки
POST /api/ade/reproduction осеменения, стельности, отёлы
POST /api/ade/animal-events перемещения, выбытия
GET /api/ade/animals выдача животных наружу- идемпотентность — повторная отправка того же события не создаёт дубль; ключ: животное + тип события + дата;
- частичный приём — непрошедшие строки отклоняются поштучно;
- прослеживаемость — каждая принятая запись хранит систему-отправителя, идентификатор пакета и время приёма.
13.5. Приём генотипов#
Файлы с чипов слишком велики для синхронного разбора, поэтому приём асинхронный: загрузка возвращает идентификатор задания, статус и протокол контроля качества запрашиваются отдельно. Обязательные метаданные — чип, сборка генома и конвенция кодирования аллелей: без них смешивание конвенций даёт тихую порчу данных, которая проявится только падением точности оценки.
POST /api/genotypes/upload file, chip, assembly, alleleCoding, laboratory
GET /api/genotypes/jobs/:id статус, принято/отклонено, причины, протокол13.6. Государственные реестры и системы управления стадом#
ВетИС «Хорриот» — реализуемо: шлюз ВетИС.API, доступ по официальному письму, тестовый контур, апробация не менее десяти рабочих дней. Сроки предсказуемы.
ФГИАС ПР — заблокировано отсутствием спецификаций. Регистрация обязательна с 01.03.2026, но публичного описания форматов обмена в открытом доступе нет. Ставить срок в план, пока спецификации не получены, нельзя.
У систем управления стадом публичного REST нет — это не интеграция по API, а набор адаптеров. Внутренний контракт адаптера один: вернуть события в форме ADE и указать источник. Тогда добавление новой системы не затрагивает ядро.
| Система | Механизм забора | Что учесть |
|---|---|---|
| DairyComp 305 | периодический прогон командной строки и разбор выгрузок | передача данных третьей стороне оформляется соглашением, подписывает ферма |
| DelPro | партнёрская интеграция либо чтение резервных копий | схема БД официально не опубликована |
| UNIFORM-Agri | ICAR ADE | самый прямой путь: стандарт реализован вендором |
| AfiFarm | по договорённости с вендором | публичной спецификации нет |
Часть V. Развёртывание и эксплуатация
14. Окружение#
14.1. Переменные окружения#
| Переменная | Назначение |
|---|---|
DATABASE_URI | строка подключения к PostgreSQL |
PAYLOAD_SECRET | подпись токенов; смена разлогинивает всех |
SEED_CONFIRM | предохранитель массовых записывающих скриптов |
PAYLOAD_DB_PUSH | принудительно выключает прямое изменение схемы |
Параметр sslmode вырезается из строки подключения и превращается в настройку TLS по правилам libpq: драйвер понимает не все значения, которые понимает psql, и расхождение проявлялось отказом подключения уже на боевом контуре.
14.2. Контейнер#
Сборка в режиме standalone, в контейнере запускается node server.js, а не npm start: лишний процесс-посредник мешает корректной остановке по сигналу. HEALTHCHECK ходит в /healthz/live, который не касается базы, — иначе неверная строка подключения превращается в «Deploy failed» вместо диагностируемой ошибки.
15. Управление схемой#
15.1. Push в разработке, миграции на бою#
В разработке Payload изменяет схему напрямую по описанию коллекций; на бою работают только миграции. Разграничение сделано по строке подключения (isLocalDatabase), а не по NODE_ENV: скрипт, запущенный с рабочей машины против боевой базы, формально не «production» — и однажды этого оказалось достаточно, чтобы схема изменилась там, где не должна была.
15.2. Рассинхронизация и её лечение#
Типичная поломка: схема в базе уже изменена, журнал миграций об этом не знает, очередная миграция падает на «ограничение уже существует» или «тип уже существует». Причина — служебная запись, которую push оставляет в payload_migrations. Данные при этом целы.
npm run db:sync # снимает служебную отметку, доводит журнал, применяет миграции
npm run doctor # семь проверок окружения и базы, только чтение
npm run db:precheck # что произойдёт до того, как оно произойдётОдна команда вместо трёх появилась не для удобства: последовательность из трёх шагов, выполняемая руками под сбоем, рано или поздно выполняется в неверном порядке.
16. Мониторинг и диагностика#
16.1. Две пробы состояния#
/healthz отвечает HTTP 200 всегда, а результат выражает полем status в теле. Это сделано намеренно: проба нужнее всего именно тогда, когда база недоступна, а панели и платформы нередко прячут тело ответа с кодом 5xx за собственной заглушкой. Для автоматической проверки живости есть отдельный /healthz/live.
16.2. Служебные ревизии#
| Команда | Что проверяет |
|---|---|
npm run doctor | окружение, схема, долгие транзакции, распухание таблиц |
npm run audit:tenancy | не отдаёт ли какой-нибудь список чужие записи |
npm run audit:pedigree | противоречия в происхождении |
npm run audit:indexes | индексы базы: недостающие и ни разу не пригодившиеся |
npm run smoke | обход всех страниц живого сервера |
npm run check:all | все двадцать пять подряд, с записью результата на вкладку «Статус» |
Полный перечень проверок с признаками — src/lib/check-registry.ts. Три признака решают всё: пишет ли она в базу (десять из двадцати пяти заводят записи и потом удаляют — на боевой книге такое гонять нельзя), нужен ли ей живой сервер (три ходят по страницам снаружи) и умеет ли её прогнать само приложение (четыре — они и попадают в ночной прогон).
Прогон на развёрнутой системе. GET /checks?token=…&label=Прод гоняет четыре пробы внутри работающего приложения и кладёт результат в коллекцию check-runs. Ключ — CHECKS_TOKEN, не короче шестнадцати знаков; без него маршрут отвечает несуществующей страницей. Код ответа говорит об исходе: 200 — сошлось, 409 — есть находки, так что ночному действию не нужно разбирать тело. На боевой машине прогон занимает около трети секунды и годится после каждой выкладки.
Прогон по боевой базе. Пятнадцать читающих проверок: DATABASE_URI=… BASE=… npm run check:all -- --readonly --label Прод. Десять пишущих туда не попадают и не попадут: обрыв посреди прогона оставил бы в книге записи, неотличимые от настоящих.
Результаты видны на вкладке «Статус» этой же страницы. Там же сказано, какие проверки не гонялись и почему. Результат старше полутора суток показывается как неизвестный, а не как зелёный: доска, показывающая вчерашнее за нынешнее, хуже отсутствующей.
Чего по-прежнему нет: автоматической сборки и модульных тестов. Прогон проверок запускается по расписанию или по выкладке, но код между выкладками никто не проверяет — это остаётся в списке ограничений.
Часть VI. Приложения
17. Приложения#
А. Стандарты#
| Стандарт | Что из него взято | Состояние |
|---|---|---|
| ICAR Section 2 — Cattle Milk Recording | схемы контрольных доек, модели лактационных кривых | спроектировано |
| ICAR Section 4 — DNA Technology | требования к генотипированию и контролю качества | спроектировано |
| ISO 11784/11785, ICAR Section 10 | средства маркирования животных | спроектировано |
| ICAR ADE (Section 15) | формат обмена данными между системами | спроектировано |
| Регламент (ЕС) 2016/1012 | форма зоотехнического сертификата и племенного свидетельства | работает |
| ГОСТ 7.79-2000 (ISO-9) | транслитерация кличек | работает |
| ВетИС «Хорриот» | маркирование и учёт животных | план |
| ФГИАС ПР | государственный учёт племенных ресурсов | план |
Б. Ограничения — честный список#
То, что нужно знать до того, как строить планы на систему. Список не сокращённый.
- Данные синтетические. 280 тысяч животных построены по реальным распределениям, но ни одно хозяйство не ведёт здесь настоящий учёт.
- Проверки данных смотрят одно хозяйство. Отчёты и сверки берут самое большое из заведённых; расхождение, которое возникает только у другого, ночным прогоном не найдётся.
- Работы с маркерами нет. Ни приёма файлов генотипирования, ни нормализации аллелей, ни QC, ни импутации, ни ssGBLUP.
- Нет сборки по коммиту и модульных тестов. Прогон проверок ставится на расписание и на выкладку (раздел 16.2), но код между выкладками не проверяется ничем.
- Битые внешние ссылки не проверяет ничто. Обход страниц знает только свои адреса; ссылка на чужой сайт, который закрылся, останется незамеченной. Расхождение этой документации с кодом — тоже.
- Интеграционный слой не реализован. Приём по ADE и приём генотипов спроектированы, эндпоинтов пока нет.
- Приём пакета — целиком. Частичный приём предусмотрен контрактом ADE, но не текущей логикой пакетов.
- Экстерьер хранится без достоверности. 18 линейных признаков и 3 композита — одиночные числа без прогноза и R.
- Обещания совместимости нет. Версия
0.16.0-alpha: структура данных может измениться.
В. Где что лежит#
| Задача | Файл или каталог |
|---|---|
| Правила доступа | src/access/index.ts |
| Родословная и инбридинг | src/lib/ancestry.ts |
| Индекс племенной ценности | src/lib/breeding-index.ts |
| Автоматические проверки | src/lib/data-checks.ts |
| Ограничения базы | src/lib/db-constraints.ts |
| Журнал правок | src/lib/animal-journal.ts |
| Видимость и замок | src/lib/visibility.ts |
| Качество книги | src/lib/book-quality.ts |
| Выпуск документов | src/actions/documents.ts |
| Служебные скрипты | src/scripts/ |
Г. Что нужно интегратору#
Если вы поставщик системы управления стадом, лаборатория или сервисная организация, порядок разговора такой.
- Определить сторону обмена. Вы отдаёте данные в книгу, забираете из неё или и то и другое. От этого зависит, нужен ли вам контракт приёма (ADE) или достаточно чтения по REST.
- Договориться об идентификаторе. Индивидуальный номер хозяйства уникален внутри хозяйства, а не глобально. Устойчивый ключ сопоставления —
uuidплюс средство маркирования. - Проверить свою модель на ограничениях. Данные извне отклоняются по тем же 28 правилам, что и введённые руками; мягкого режима для API нет.
- Учесть правовую сторону. Передача данных третьей стороне у части вендоров оформляется отдельным соглашением, и подписывает его хозяйство, а не разработчик.
Вопросы по интеграции — через Ассоциацию: контакты в подвале страницы.
Д. Журнал решений#
Спорные развилки записываются отдельным документом репозитория docs/reshenya.md — с разбором отвергнутых вариантов и причины отказа. Сейчас там 191 запись. Это не история изменений, а объяснение, почему сделано так: следующему человеку не нужно тратить вечер на ту же мысль.