хранилище переехало с PocketBase на SQLite со своим каталогом файлов

- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose
  под файловым замком, одна миграция начальной схемы вместо семи прежних
- транспорт переписан на net/http: свои слои, свой ограничитель частоты,
  отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли
- по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета
  читается справа налево, узнавание известного идёт читающим пулом
This commit is contained in:
av
2026-08-23 08:06:04 +03:00
parent 1edf8cb225
commit c9b7765646
118 changed files with 11668 additions and 6679 deletions
+67 -17
View File
@@ -10,7 +10,7 @@
перехода, неделимость захвата и срок его протухания, условие записи результата
держателем захвата, нарастающая пауза перед повтором, число воркеров настройкой,
журнал событий записи и молчание конвейера наружу: обращений к отправителю он не
делает вовсе, и свой исход тот узнаёт опросом готовности.
делает вовсе, и свой исход тот узнаёт карточкой записи.
Сознательно не описаны: освобождение ресурсов внешних клиентов и **какие отказы
считаются приговором записи, а какие поводом к повтору**. Второе — не пробел
@@ -20,6 +20,7 @@
потому что требование без проверки — предположение, а не норма. Первая задача,
которая трогает любое из перечисленного, дописывает его сюда.
## Requirements
### Requirement: Пустой прогон воркера — не отказ
Воркер SHALL отличать «пригодной к работе записи сейчас нет» от отказа шага. На
@@ -87,8 +88,9 @@
### Requirement: Захват задачи неделим
Захват записи воркером SHALL быть одним неделимым шагом хранилища: выбор
подходящей записи и пометка её захваченной MUST происходить вместе.
Захват записи воркером SHALL быть одним неделимым запросом к базе: выбор
подходящей записи и пометка её захваченной MUST происходить вместе, одним
оператором с возвратом.
Захват MUST возвращать **идентификатор записи и признак этого захвата**, а не
перечень её колонок. Колонки записи шаг читает сам, обычным чтением. Иначе
@@ -99,7 +101,8 @@
**Признак захвата MUST быть значением, уникальным для каждого захвата**, а не
признаком занятости. Условие записи результата сверяет именно это значение:
захват, перевыданный другому — по протуханию срока или после того, как человек
снял признак остановки в панели, — обязан обращать запись первого в отказ.
снял признак остановки подкомандой оснастки, — обязан обращать запись первого в
отказ.
Условие, проверяющее лишь непустоту признака или срок, пропустило бы обоих, и
два шага записали бы в одну запись по очереди, испортив её результат.
@@ -170,15 +173,19 @@
Шаг MUST записывать только те поля, которыми распоряжается сам. Запись он держит
снимком с момента захвата и до записи — это часы, — и безусловная запись снимка
стёрла бы всё, что владелец правил в панели за это время: молча, без строки в
журнале и без отказа в панели. Владелец увидел бы успешное сохранение и был бы
уверен, что правка на месте. Владелец записи, заголовок, краткое описание и темы
стёрла бы всё, что владелец правил за это время: молча, без строки в журнале и
без отказа тому, кто правил. Владелец записи, заголовок, краткое описание и темы
конвейер MUST не трогать.
Возвращает остановленную запись в работу сегодня владелец сервиса подкомандой
оснастки — своего экрана для этого у сервиса нет. Норму это не меняет: она
написана про поля, а не про того, чьей рукой правка сделана, и переживёт
появление экранов.
#### Scenario: Правка владельца пережила сохранение шага
- **GIVEN** шаг держит захваченную запись
- **AND** владелец за это время изменил в панели поле, которого шаг не касается
- **AND** владелец за это время изменил поле, которого шаг не касается
- **WHEN** шаг записывает свой результат
- **THEN** результат шага записан
- **AND** правка владельца на месте
@@ -346,10 +353,36 @@ MUST расти с числом её отказов до объявленног
и машинный текст отказа.
Снятие признака SHALL возвращать запись в работу **с того рубежа, где она
стояла**, и MUST сбрасывать число отказов, паузу **и время входа в рубеж**.
Время входа сбрасывается по той же причине, что и остальные сторожа: запись,
простоявшая остановленной дольше предела, иначе останавливалась бы снова первым
же захватом, и перезапуск не работал бы вовсе.
стояла**, и MUST сбрасывать **всё, чем прошлый прогон её удерживал**:
- признак остановки — время остановки, причину и машинный текст отказа;
- признак захвата и срок его протухания;
- число отказов;
- паузу перед повтором;
- время входа в рубеж.
Перечень назван целиком и в одном месте, потому что забытое поле не даёт ни
отказа, ни строки в журнале. Оставленный признак захвата держит запись занятой до
протухания срока и отдаёт её потом чужому шагу — тому, чей результат условен по
прежнему значению. Оставленное время входа в рубеж останавливает запись снова
первым же захватом, если остановленной она простояла дольше предела, и перезапуск
не работает вовсе. Оставленные отказы и пауза откладывают первый же прогон на
накопленный срок.
Возврат в работу MUST идти **через домен**: тот, кто его делает, называет запись,
а поля выше сбрасывает домен одним действием. Правка колонок мимо домена
повторяет перечень вторым местом, и второе место расходится с первым молча.
Возврат в работу MUST писать событие журнала записи с происхождением «человек».
Иначе запись, побывавшая остановленной и вернувшаяся в работу, неотличима в
журнале от записи, которую конвейер вёл без остановок, а происхождение события
перестаёт различать что-либо: другого писателя, кроме конвейера, у журнала не
остаётся.
Инструментом возврата сегодня служит подкоманда набора инструментов
разработчика: панели у сервиса нет, а экраны владельца приносят отдельные задачи.
Норма написана про поля и про домен, а не про инструмент, и появление экрана её
не трогает.
Прежние состояния отказа и смерти MUST не заводиться заново: обе причины
восстанавливаются одинаково — снятием признака, — и различие между ними
@@ -389,6 +422,19 @@ MUST расти с числом её отказов до объявленног
- **THEN** число отказов, пауза и время входа в рубеж сброшены
- **AND** ближайший захват выдаёт запись, а не останавливает её снова
#### Scenario: Возврат в работу освобождает захват
- **GIVEN** запись остановлена, и признак прошлого захвата на ней стоит
- **WHEN** признак остановки снимают
- **THEN** признака захвата и срока его протухания на записи нет
- **AND** ближайший захват выдаёт запись, не дожидаясь протухания прежнего срока
#### Scenario: Возврат в работу виден в журнале записи
- **GIVEN** запись остановлена
- **WHEN** её возвращают в работу
- **THEN** в журнале событий записи есть событие с происхождением «человек»
### Requirement: Время в рубеже ограничено
У аудиозаписи SHALL быть время входа в рубеж, и оно MUST ставиться только при
@@ -608,8 +654,13 @@ MUST не быть привязаны к отдельному шагу: кажд
Шаг конвейера SHALL доводить запись до достигнутого рубежа и MUST не обращаться
к отправителю вовсе — ни с готовым текстом, ни с сообщением о неудаче. Исход
своей записи владелец узнаёт **карточкой записи** и в панели владельца сервиса;
адрес карточки и содержимое ответа нормирует capability `archive`.
своей записи владелец узнаёт **карточкой записи**; адрес карточки и содержимое
ответа нормирует capability `archive`.
Владелец сервиса узнаёт исход журналом и журналом событий записи. Панели, где он
видел бы то же строкой таблицы, у сервиса нет — она ушла вместе со встроенным
хранилищем, и второго канала наблюдения это не отняло: журнал событий пишется
по-прежнему, а читается запросом к базе, пока экрана нет.
Держатель нормы сменился вместе с убранным опросом готовности: прежде исход
отдавал адрес опроса, нормированный capability `intake`, и адреса этого больше
@@ -628,8 +679,8 @@ MUST не быть привязаны к отдельному шагу: кажд
спрашивает, об остановке не узнаёт.
Записи, которой этот канал недоступен, не бывает: у каждой записи есть владелец,
и карточка отдаёт ему её исход. Держится это обязательностью владельца в схеме
хранилища — норму держит capability `storage`.
и карточка отдаёт ему её исход. Держится это обязательностью владельца в схеме
норму держит capability `storage`.
#### Scenario: Готовый текст отправителю не уходит
@@ -644,4 +695,3 @@ MUST не быть привязаны к отдельному шагу: кажд
- **WHEN** владелец записи спрашивает её карточку
- **THEN** ответ несёт достигнутый рубеж, признак остановки и её причину
- **AND** в журнале владельца сервиса есть запись об остановке с причиной