Эволюция продукта

Техническое описание системы для тех, кому предстоит с ней работать: модель данных, процессы, архитектура, контракты обмена, развёртывание и — отдельно — ограничения.

Часть I. Введение и обзор

1. О системе#

1.1. Какую задачу решает#

«Племенная книга» — информационная система племенного учёта скота. Она ведёт учёт племенных животных, хранит происхождение и продуктивность, считает племенную ценность, выпускает племенные документы и — это главное — отвечает на вопрос, кто ручается за каждую цифру. Первая книга на ней — голштинская, её ведёт профильная ассоциация.

Ценность племенного животного подтверждается данными о предках и потомстве. Сегодня эти данные разбросаны: часть в системе управления стадом, часть в бумажных свидетельствах, часть в таблицах зоотехника. Покупатель, глядя на цифру продуктивности, не знает её происхождения — сама ферма её ввела или она подтверждена независимой стороной. Система устроена вокруг этой разницы: у каждой записи есть уровень достоверности и история изменений.

1.2. Кому адресован этот документ#

Прежде всего техническим специалистам, которым предстоит решить одну из двух задач: построить собственную интеграцию со своей стороны или оценить Ассоциацию как технологического партнёра. Это не только хозяйства — это лаборатории генотипирования, поставщики систем управления стадом, сервисные организации по воспроизводству, разработчики отраслевых сервисов.

Документ отвечает на четыре практических вопроса: какая модель данных вас ждёт, какие контракты обмена уже есть и какие спроектированы, что нужно для развёртывания собственного контура, и — отдельным приложением — чего в системе нет. Последнее не менее важно: интеграция ломается о недосказанное, а не о недостающее.

Отметки у разделов читаются так: работает — есть в коде и работает; спроектировано — контракт продуман и записан, реализации нет; план — направление известно, сроки нет.

1.3. Принципы#

  • Владелец распоряжается своими данными. Публичность записи включает хозяйство, а не Ассоциация. Ассоциация ручается за данные перед другими, но не открывает их за владельца.
  • У цифры есть происхождение. Уровень достоверности, автор, время правки и предыдущее значение хранятся вместе с записью, а не выводятся из журналов постфактум.
  • Правила предметной области живут в базе. Возраст, доли, знаки, запрет быть себе родителем — ограничения, которые нельзя обойти ни импортом, ни служебным скриптом.
  • Прошлое не редактируется. Документы отзываются, а не удаляются; правки дописываются в журнал; типы событий выводятся из обращения, а не стираются.
  • Стандарт вместо собственного формата. Обмен проектируется на ICAR ADE, документы — по форме Регламента (ЕС) 2016/1012. Свой формат пришлось бы объяснять каждому партнёру отдельно.
  • Отказ объясняется построчно. Импорт называет номер строки, идентификатор и причину. «Часть данных не принята» без указания какой — не сообщение, а тупик.

1.4. Текущее состояние и что это значит#

Версия 0.20.0-alpha. Работающий прототип: интерфейс, модель данных и расчёты действуют на объёме 280 964 животных, 42 хозяйств и 152 стад, но данные синтетические — построены по реальным распределениям, и ни одно хозяйство пока не ведёт здесь настоящий учёт.

Для интегратора это значит следующее. Модель данных и контракты чтения можно изучать и на них ориентироваться, но обещания обратной совместимости нет: ноль в начале номера версии именно об этом. Автоматической сборки, тестов и мониторинга нет — служебные ревизии запускаются вручную. Полный список ограничений — в приложении Б.

2. Быстрый старт#

2.1. Требования к окружению#

Приложение работает на одном сервере с реляционной базой данных и требует около 2 ГБ свободной памяти на сборку. Внешних служб не нужно: почта, хранилище файлов и очереди в текущей версии не используются — всё, что системе нужно для работы, находится внутри неё.

СлойУстройство
ПриложениеСерверная отрисовка страниц: разметка собирается на сервере, браузер получает готовую
ДанныеРеляционная база; схема выводится из описания предметной области, а не пишется отдельно
ИнтерфейсКомпонентная сборка страниц с общей палитрой и типографикой
ВыпускСамодостаточный образ: сервер, зависимости и собранные страницы в одном

Названия и версии используемых библиотек здесь не приводятся намеренно. Точная версия — это готовый запрос к перечню известных уязвимостей: она экономит нападающему ровно тот шаг, ради которого он и приходит на страницу с описанием устройства. Заказчику для оценки решения важно, как система устроена, а не на чём набрана; тем, кому нужен состав, — партнёру по интеграции, аудитору, приёмочной комиссии, — он выдаётся по запросу.

2.2. Запуск#

Развёртывание описано в документации репозитория и здесь не приводится. Прежде тут стояли команды установки целиком — вместе с именами служебных переменных и с тем, какая из них снимает защиту от перезаписи базы. Это инструкция для человека, у которого есть доступ к исходникам, и полезна она была ровно ему; всем остальным она сообщала, что именно спрашивать у системы и какой предохранитель искать.

Что важно знать снаружи: наполнение базы демонстрационными данными удаляет существующие записи и потому закрыто отдельным подтверждением — запустить его «по привычке» на боевом контуре нельзя. Разбор рассогласования схемы и журнала миграций — в главе 15.

2.3. Роли#

РольЧто видит и что может
farm — хозяйствосвоё стадо целиком, публичные записи чужих; ведёт данные, управляет публичностью, отвечает на запросы доступа, подаёт заявки на верификацию
expert — эксперт Ассоциациичитает данные всех хозяйств, разбирает пакеты и заявки, оставляет замечания; править чужие данные не может
calculator — расчётная организацияподрядчик, считающий племенную ценность: читает по обмену данные всех хозяйств, присылает обратно только оценки; публиковать их не может
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пакеты загрузки, журнал правок, заявки на верификацию и на доступ
Доступ и согласияaccess-grants, share-links, genotype-consentsточечный доступ, ссылки на просмотр, согласие владельца на обработку генотипа
Документы и НСИdocuments, events, media, справочникивыданные документы, лента событий, файлы, породы и линии

3.2. Животное и его идентификаторы#

Карточка разбита на смысловые блоки: идентификация, происхождение, фенотип и продуктивность, генетика, движение, оценка. Идентификаторов несколько, и они не взаимозаменяемы — это первое, обо что спотыкается интеграция.

ПолеЧто это
uuidGUID записи. Присваивается при создании, не меняется никогда, не зависит от номеров хозяйства. Устойчивый ключ сопоставления при обмене
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, — и за ним следит ревизия audit:grants.

Тому, у кого нет учётной записи, выдаётся ссылка на просмотр (share-links, страница /share/[token]) — с тем же сроком и той же областью.

Техническая сложность здесь была в том, что проверка «есть ли действующий грант» попадает на горячий путь — страницу книги и карточку. Условие на связанную таблицу превращается в соединение, и на базе замера в 280 тысяч записей это стоило секунд. Отсюда отдельный переключатель в настройках: он снимает соединение целиком, если однажды окажется, что цена выросла.

5.5. Геномная оценка и согласие владельца#

До сих пор видимость записи повторяла видимость животного целиком: открыто животное — открыты все его записи. Для дойки и взвешивания это верно, для оценки перестало быть верным. Геномная оценка молодого быка до появления дочерей публикуется только с согласия владельца — так решила Ассоциация и так принято в мире: пока у быка нет дочерей, это прогноз, и владелец вправе не показывать его конкурентам, покуда сам не решит выводить быка на рынок.

Кто смотритЧто видит
владелецсвои оценки всегда — иначе он не узнает, что публиковать
Ассоциациявсё: она отвечает за расчёт
по точечному доступупо гранту: это адресное разрешение того же владельца, оно сильнее умолчания о публикации
постороннийгеномную — только объявленную Ассоциацией; остальные — как прежде

Прячется прогноз, а не результат. Оценка по дочерям — итог работы на данных всей популяции, дочери уже подоились, числа существуют; скрывать их незачем. Родословная оценка выводится из оценок родителей, которые и так на виду.

Скрытая оценка не показывается нигде, а не только в истории. Снимок действующей оценки лежит копией в карточке животного, и читают его два десятка мест — таблица книги, рейтинг, сравнение быков, отчёты, выгрузки, сертификаты. Закрывать их поштучно значило бы завести двадцать мест, где решается один вопрос: девятнадцать поправят, одно забудут. Поэтому неопубликованная геномная оценка просто не переносится в снимок — книга ею не пользуется ни в чём, включая расчёт индекса и рейтинг. При публикации снимок ложится сам: это тот же хук записи.

Согласие — предмет учёта, а не бумага в папке. SNP-профиль принадлежит владельцу животного, и Ассоциация обрабатывает его по согласию с названным объёмом: хранить, использовать в расчёте, передавать расчётной организации, публиковать результат. Передача третьему лицу названа отдельно намеренно — без неё согласие было бы нарушено в первый же день работы с подрядчиком, не по злому умыслу, а потому что не спросили. Пустое поле животного означает согласие на всё стадо: хозяйство с пятью тысячами голов не станет оформлять пять тысяч бумаг, и требование поштучных согласий означало бы отсутствие согласий вовсе.

Отзыв прекращает будущее использование и не переписывает прошлое. Владелец вправе забрать согласие, и с этого дня генотип не участвует ни в чём. Но оценки, уже посчитанные с его участием, отозвать нельзя: геномная оценка популяционная, генотип одного животного входит в расчёт для тысяч чужих, и «убрать его задним числом» означало бы пересчитать и переопубликовать всю книгу за все годы. Правило записано кодом: проверка согласия принимает дату, и на день до отзыва отвечает «действовало». Владелец должен узнать об этом до того, как поставит подпись, а не после отзыва.

Проверка одна на всю систему — src/lib/genotype-consent.ts. Спрашивают её при расчёте, при передаче и при показе; напиши условие трижды, и через полгода одно из трёх забудут поправить: расчёт сочтёт согласие действующим, а показ истёкшим. Второе безобидно, первое — нарушение.

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 — основной для регулярного потока. Разбирает файл, заводит пакет, обновляет существующих животных по индивидуальному номеру и объясняет каждую непринятую строку.
  • Ручной ввод — для того, что файлом не приходит: купленное животное, расхождение с бумажным свидетельством, событие по ходу дела. Пишется в журнал правок.
  • APIPOST /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 записей своей организации. Ограничение осознанное: выгрузка в сотни тысяч строк держит соединение минутами и всё равно заканчивается таймаутом посредника.

10.2. Перенос между контурами#

Порядок для нового кода всегда один: выкладка → миграции → наполнение данными. Обратный порядок даёт ошибку вида column animals.for_sale does not exist — код знает про поля последнего изменения, база знает только про применённые миграции.

Применение миграций идемпотентно: применённые пропускаются, применяются только новые. Повторный запуск поэтому безопасен — это важное свойство, потому что при неудачной выкладке запускают повторно почти всегда.

Часть IV. Техническая архитектура и реализация

11. Технологическая основа#

11.1. Стек и почему он такой#

Основа выбрана по одному признаку: из единственного описания предметной области она даёт сразу три вещи — схему базы, служебный интерфейс редактирования и программный интерфейс, причём с теми же правами доступа, что и у страниц. Для системы, где предметная модель большая, а бюджет на инфраструктуру маленький, это решающий фактор: правила доступа пишутся один раз и действуют везде. Дублировать их для страниц, для служебной панели и для обмена — значит однажды поправить в двух местах из трёх.

Страницы собираются на сервере и ходят в базу напрямую, минуя собственный HTTP-слой: обращаться к своему же интерфейсу по сети ради данных, которые лежат в том же процессе, — лишний круг и лишняя точка отказа. Формы работают серверными действиями. Кода, исполняемого браузером, в приложении мало по замыслу, а не по недосмотру.

Названия используемых библиотек и их версии здесь не приводятся — разбор в главе 2.1.

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 тысяч записей невозможно — это проверено на практике, а не предположено.

Важная оговорка о том, чем это не является. Индекс — свёртка готовых оценок с весами, а не оценка племенной ценности. Сами EBV книга принимает извне: из расчётного центра, обменом ADE, файлом или как зарубежную оценку. Пока средние и наследуемости по признакам заимствованы: для линейки индекса это допустимо, для собственной оценки — нет, и разговор о своей базе сравнения идёт отдельно (приложение Б).

12.3. Что записано вместе с оценкой#

Число без обстоятельств непроверяемо, и каждое поле рядом с ним отвечает на вопрос, который однажды зададут вслух — обычно в тот день, когда бык за квартал потерял четыреста килограммов молока.

ПолеНа какой вопрос отвечает
sourceоткуда запись пришла в книгу: расчётный центр, файл, заграница, обмен ADE. Это канал, а не ответственный
calculatedByкто получил число: подрядчик Ассоциации, региональный центр, CDCB, Lactanet. По обмену берётся из meta.creator
publisher, publishedAtкто объявил число от своего имени и когда. Пусто — запись принята, но Ассоциация под ней не подписывалась
calculationчем посчитано: по маркерам, по дочерям, по родословной. От этого зависит выбор действующей оценки
daughters, herdsна чём стоит оценка по потомству. Порознь бессмысленны: двести дочерей в одном стаде — оценка стада, а не быка
baseVersionотносительно какой базы посчитано отклонение
profile, profileRevisionкаким набором весов свёрнут индекс и какой его редакцией: профили правят, и после правки меняются все индексы книги

Разделение «кто посчитал» и «кто опубликовал» следует из порядка Ассоциации: она публикует племенную ценность от своего имени, а расчёт заказывает у подрядчика. Одного поля здесь мало — «принято обменом ADE» не говорит ни кто считал, ни кто отвечает.

Пара «профиль + редакция» и версия базы нужны, чтобы разделить два движения, которые иначе неразличимы. В апреле 2025 у американского быка PTA молока упал с +143 до −539, а сводный индекс при этом вырос: в одну публикацию попали и смена базы сравнения, и пересмотр формулы индекса. Дочерей у быка за тот же квартал прибавилось на тысячу — то есть он не ухудшился ни на грамм. Без обоих полей такое читается как «животное изменилось».

Число дочерей и стад стандарт ADE не предусматривает ни на уровне значения, ни на уровне ресурса — сверено с вендорной копией схем и с веткой ADE-1 у adewg. Книга принимает их расширением и отдаёт по своей схеме; описание для партнёров — на странице обмена.

12.4. Профили весов и коррелированный отклик#

Профили — то, ради чего расчёт сделан настраиваемым. Одному хозяйству важнее белок, другому продуктивное долголетие; рейтинг перестраивается под экономику хозяйства, оставаясь сопоставимым между хозяйствами за счёт общей базы сравнения.

Отдельно считается коррелированный отклик: что произойдёт с остальными признаками при отборе по выбранному. Без него профиль весов — способ выстрелить себе в ногу: усиление одного признака тянет за собой другие, и не всегда в нужную сторону.

12.5. Автоматические проверки данных#

Семьдесят семь правил, собранных реестром src/lib/checks-registry.ts: согласованность дат рождения и отёлов, правдоподобность величин (удой 500…25 000 кг за лактацию и подобные границы), разрывы и противоречия в происхождении, несовместимые состояния. Правила намеренно отделены от ограничений базы: ограничение запрещает невозможное, правило указывает на подозрительное — второе не должно блокировать запись, но должно попадать на глаза эксперту.

12.6. Геномный конвейер#

Из геномного конвейера в коде есть две вещи, и обе не про маркеры: расчёт инбридинга по родословной и механика пакетной загрузки с протоколом ошибок. Тип загрузки genomics в перечне заведён, обработчика для него нет.

Не реализовано ничего из работы с самими маркерами: приём файлов генотипирования, нормализация аллелей, контроль качества, импутация, матрицы родства, ssGBLUP. Это сказано прямо, потому что «геномная оценка» в описании системы обычно означает совсем другое.

Одна особенность хранения, которую стоит знать заранее: у продуктивных признаков и признаков здоровья есть пара «прогноз + достоверность», у ИПЦ к ней добавлен процентиль, а признаки экстерьера хранятся одиночными числами без прогноза и без R. Когда оценка начнёт считаться внутри системы, экстерьеру понадобится та же пара — иначе достоверность экстерьерной части индекса негде будет показать.

13. API и интеграции#

13.1. 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 авторизованный получал первичные данные чужого хозяйства. Предупреждение снято не потому, что стало неудобным, а потому что дыра закрыта: правила переписаны, и за ними следят ревизии check:security и 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
Проба готовностисостояние базы и окружения; закрыта ключом — служебный адрес не публикуется
Проба живучестиоткрыта, отвечает «ok» и не касается базы: её дёргает контейнер

13.4. Обмен в форме ICAR ADE#

Контракт интеграционного слоя — открытый стандарт ICAR ADE (OpenAPI 3.1, JSON Schema 2020-12). Он уже реализован рядом вендоров, и Section 15 Guidelines прямо отсылает к нему; собственный формат пришлось бы объяснять каждому партнёру отдельно. Обмен работает — и на чтение, и на приём.

Адреса взяты не из пересказа стандарта, а из его собственных схем путей, и потому выглядят непривычно для тех, кто ждёт «эндпоинт на каждое событие»: раздел стандарта стоит в пути последним сегментом, а не в имени адреса.

GET  /ade/v1/locations                                    доступные локации
GET  /ade/v1/locations/{scheme}/{id}/{collection}         выдача раздела
POST /ade/v1/locations/{scheme}/{id}/{collection}         приём одного ресурса
POST /ade/v1/batches/locations/{scheme}/{id}/{collection} приём пакета
GET  /ade/v1/datasets                                     наборы данных
GET  /ade/v1/datasets/{dataset}/changes                   что изменилось с прошлого раза

На приём открыты семь разделов: test-day-results, parturitions, inseminations, weights, milking-visits, diagnoses, breeding-values. Остальные отвечают не «неизвестно», а называют причину, по которой книга их не ведёт: «неизвестная коллекция» читается как незнание стандарта и толкает интегратора пробовать снова и писать письмо.

  • идемпотентность — повторная отправка того же ресурса не создаёт дубль; ключ — пара meta.source и meta.sourceId, поэтому второй из них у нас обязателен, хотя в стандарте необязателен;
  • частичный приём — пакет отвечает icarBatchResult и всегда кодом 200, даже когда часть записей не принята: код относится к обработке пакета, а не к его содержимому;
  • прослеживаемость — каждая принятая запись хранит систему-отправителя и её идентификатор ресурса; удаление оставляет след, иначе партнёр, ведущий свою копию книги, никогда не узнает, что запись отозвали;
  • расширение по оценкам — числа дочерей и стад в стандарте нет ни на уровне значения, ни на уровне ресурса; книга принимает их расширением, а расчётчика берёт из штатного meta.creator. Описание для партнёров — на странице обмена.

Права по обмену устроены так же, как в самой книге: хозяйство ходит по своим локациям, Ассоциация по всем, расчётная организация — по всем на чтение, но пишет только breeding-values. Заслон стоит в шлюзе обмена, а не в правах коллекций: приём идёт в обход правил доступа — иначе он не смог бы писать в чужие хозяйства, ради чего и заведён.

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-AgriICAR ADEсамый прямой путь: стандарт реализован вендором
AfiFarmпо договорённости с вендоромпубличной спецификации нет

Часть V. Развёртывание и эксплуатация

14. Окружение#

14.1. Переменные окружения#

НастройкаНазначение
Подключение к базеадрес, имя базы и учётные данные
Ключ подписиподпись токенов входа; смена разлогинивает всех
Подтверждение наполненияпредохранитель массовых записывающих действий
Режим схемыпринудительно выключает прямое изменение схемы

Имена переменных здесь названы по назначению, а не буквально. Точное имя ничего не объясняет читателю снаружи, зато точно указывает, что спрашивать у системы: список имён — это половина работы того, кто ищет неверно настроенное развёртывание. Обслуживающему персоналу имена известны из документации репозитория.

Отдельная тонкость про TLS: режим шифрования вырезается из строки подключения и превращается в настройку соединения нашими силами. Драйвер понимает не все значения, которые понимает штатный клиент базы, и расхождение проявлялось отказом подключения уже на боевом контуре — то есть там, где его труднее всего разбирать.

14.2. Контейнер#

Образ самодостаточный: сервер, зависимости и собранные страницы лежат внутри, снаружи нужна только база. В контейнере запускается сам сервер, без обёртки менеджера пакетов: лишний процесс-посредник не передаёт сигнал остановки дальше, и контейнер вместо остановки убивают по таймауту.

Проба живучести контейнера намеренно не касается базы. Иначе неверная строка подключения превращается в «Deploy failed»: контейнер не поднимается вовсе, и причину негде посмотреть — вместо диагностируемой ошибки получается молчание. Состояние базы отдаёт отдельная проба готовности, закрытая ключом.

15. Управление схемой#

15.1. Push в разработке, миграции на бою#

В разработке схема приводится к описанию предметной области напрямую; на бою работают только миграции. Разграничение сделано по строке подключения — местная база или нет, — а не по признаку режима сборки: скрипт, запущенный с рабочей машины против боевой базы, формально не «боевой», и однажды этого оказалось достаточно, чтобы схема изменилась там, где не должна была. Признак грубый и ошибиться может только в безопасную сторону: не привести схему там, где было можно.

15.2. Рассинхронизация и её лечение#

Типичная поломка: схема в базе уже изменена, журнал миграций об этом не знает, очередная миграция падает на «ограничение уже существует» или «тип уже существует». Причина — служебная отметка, которую прямое приведение схемы оставляет в журнале миграций. Данные при этом целы.

ДействиеЧто делает
db:syncснимает служебную отметку, доводит журнал, применяет миграции
doctorсемь проверок окружения и базы, только чтение
db:precheckчто произойдёт до того, как оно произойдёт

Одна команда вместо трёх появилась не для удобства: последовательность из трёх шагов, выполняемая руками под сбоем, рано или поздно выполняется в неверном порядке.

16. Мониторинг и диагностика#

16.1. Две пробы состояния#

Проба готовности отвечает успехом всегда, а результат выражает полем в теле ответа. Это сделано намеренно: проба нужнее всего именно тогда, когда база недоступна, а панели и платформы нередко прячут тело ответа с кодом ошибки за собственной заглушкой — и диагностика оказывается недоступна ровно в том случае, ради которого написана.

Проба закрыта ключом, и адрес её здесь не назван. Она отдаёт карту развёртывания: адрес и имя базы, под каким именем переменной приехала строка подключения, режим шифрования, отпечаток сборки, докуда доехали миграции. Пароли в ответе замаскированы — но замаскированный пароль при известных хосте, порте, имени базы и пользователе экономит нападающему всё, кроме последнего шага. Годами этот ответ отдавался любому, кто набрал адрес.

Проба живучести — отдельная и открытая: её дёргает контейнер, у которого ключа нет и быть не должно. Она отвечает одним словом «ok» и не говорит ни имени системы, ни времени работы: балансировщик смотрит на код ответа, а время работы выдаёт момент последней выкладки.

16.2. Служебные ревизии#

КомандаЧто проверяет
doctorокружение, схема, долгие транзакции, распухание таблиц
audit:tenancyне отдаёт ли какой-нибудь список чужие записи
audit:pedigreeпротиворечия в происхождении
audit:indexesиндексы базы: недостающие и ни разу не пригодившиеся
smokeобход всех страниц живого сервера
check:allвсе 71 подряд, с записью результата на вкладку «Статус»

Полный перечень проверок с признаками — src/lib/check-registry.ts.Три признака решают всё: пишет ли она в базу (12 из 71 заводят записи и потом удаляют — на боевой книге такое гонять нельзя), нужен ли ей живой сервер (5 ходят по страницам снаружи) и умеет ли её прогнать само приложение (9 — они и попадают в ночной прогон).

Прогон на развёрнутой системе. Закрытый служебный маршрут гоняет пробы внутри работающего приложения и кладёт результат в книгу прогонов. Маршрут требует ключа не короче шестнадцати знаков; без ключа он отвечает несуществующей страницей — снаружи закрытая ручка обязана быть неотличима от неверного адреса. Код ответа говорит об исходе, так что ночному действию не нужно разбирать тело. На боевой машине прогон занимает около трети секунды и годится после каждой выкладки.

Прогон по боевой базе. Пятнадцать читающих проверок запускаются отдельно, в режиме только для чтения. Десять пишущих туда не попадают и не попадут: обрыв посреди прогона оставил бы в книге записи, неотличимые от настоящих.

Результаты видны на вкладке «Статус» этой же страницы. Там же сказано, какие проверки не гонялись и почему. Результат старше полутора суток показывается как неизвестный, а не как зелёный: доска, показывающая вчерашнее за нынешнее, хуже отсутствующей.

Чего по-прежнему нет: автоматической сборки и модульных тестов. Прогон проверок запускается по расписанию или по выкладке, но код между выкладками никто не проверяет — это остаётся в списке ограничений.

Часть 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 964 животных построены по реальным распределениям, но ни одно хозяйство не ведёт здесь настоящий учёт.
  • Проверки данных смотрят одно хозяйство. Отчёты и сверки берут самое большое из заведённых; расхождение, которое возникает только у другого, ночным прогоном не найдётся.
  • Работы с маркерами нет. Ни приёма файлов генотипирования, ни нормализации аллелей, ни QC, ни импутации, ни ssGBLUP.
  • Нет сборки по коммиту и модульных тестов. Прогон проверок ставится на расписание и на выкладку (раздел 16.2), но код между выкладками не проверяется ничем.
  • Битые внешние ссылки не проверяет ничто. Обход страниц знает только свои адреса; ссылка на чужой сайт, который закрылся, останется незамеченной. Расхождение этой документации с кодом — тоже.
  • Приём генотипов не реализован. Обмен по ADE работает — и на чтение, и на приём, — но сами SNP-профили книга не принимает: ни файлов генотипирования, ни нормализации аллелей, ни контроля качества, ни импутации. Книга знает, что животное чипировано, и принимает геномную оценку числом; работы с маркерами в ней нет.
  • Племенная ценность не рассчитывается. Книга принимает готовые EBV от расчётного центра, обменом или файлом, а сама считает поверх них селекционный индекс — линейную свёртку с весами профиля. Матрицы родства, геномной матрицы и решения уравнений смешанной модели нет; средние и наследуемости по признакам пока заимствованы, своих компонент дисперсии у книги не посчитано. Собственный инбридинг по родословной (формула Райта, девять колен) — единственная своя генетическая математика.
  • База сравнения не объявлена. Справочник баз с датами введения готов и учитывается показом истории оценок, но состав базы и срок пересмотра — решение Ассоциации, и оно пока не принято. Оценка есть отклонение от базы: пока база не названа, собственный расчёт строить не на чем.
  • Приём пакета — целиком. Частичный приём предусмотрен контрактом ADE, но не текущей логикой пакетов.
  • Экстерьер хранится без достоверности. 18 линейных признаков и 3 композита — одиночные числа без прогноза и R.
  • Обещания совместимости нет. Версия 0.20.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 запись. Это не история изменений, а объяснение, почему сделано так: следующему человеку не нужно тратить вечер на ту же мысль.

Вернуться к началу документа