docs/ по канону 12: паспорт с целью проекта, архитектура сегодняшнего устройства, схема хранилища, модель угроз, конвенции кода, журнал ревью. Конвенции перенесены из jellybit; места, где код им не следует, помечены строкой «Расхождение» как объявленный долг. tasks/ с роадмапом: две достигнутые цели, две запланированные (веб и многопользовательский режим), два направления (все форматы, долгие записи) и пять задач в беклоге. openspec/config.yaml — маршрутизатор с адресами документов, спек пока нет. CLAUDE.md переписан по форме канона: инварианты с severity, семантика гейта, запреты с путями. Taskfile получил task gate.
4.6 KiB
4.6 KiB
Конвенция: база данных и идентификаторы
Как мы устраиваем таблицы и ключи. Актуальная схема — ../database.md.
Взято из проекта jellybit целиком. Сегодняшний код transcriber этому не
следует ни в одном пункте: ключи — UUID v4, а не ULID; время — time.Now() по
месту вызова в локальной зоне, а не единой точкой в UTC; единой точки генерации
и разбора нет. Правила действуют на новый код; переписывание существующего —
отдельная работа, и до неё расхождение читается как долг, а не как нарушение.
Механизировано: ничего. Ни правила линтера, ни теста-сканера под эти пункты в transcriber нет.
Первичные ключи — ULID, не автоинкремент
- PK сущности — TEXT ULID (26 символов Crockford base32), генерируется приложением в момент создания записи.
- Почему ULID: сортируем по времени создания (
ORDER BY id= хронология), компактен и удобен в URL и логах (без дефисов — grep и двойной клик берут id целиком), глобально уникален между таблицами — поиск по голому id находит все записи сущности в логах. - Точка генерации и разбора одна: создание — при вставке записи в репозитории, разбор — на входных границах. Самодельных генераторов по месту вызова не заводим.
Канонический вид — lowercase
- Генерим и храним id в нижнем регистре. Сравнение строк в SQLite побайтовое, поэтому любой внешний id (URL, форма, поле запроса) обязательно проходит разбор до запроса к БД — разбор проверяет формат и нормализует регистр (base32 ULID нечувствителен к регистру при декодировании).
- Синтаксически неверный id считаем несуществующей сущностью (404), без похода в БД.
Естественные и составные ключи — для деталей
- У таблиц-деталей и связей допустим естественный или составной ключ вместо ULID, когда он есть по природе данных. Отдельный ULID там — мёртвый вес.
- Прочие генерируемые идентификаторы — тем же способом, что и ключи сущностей: единый формат, сортируемость, корреляция в логах.
Прочее
- Enum-поля (
state,source, …) — обычныйTEXTбезCHECK; допустимые значения держит код. - Временные метки —
TEXTв RFC 3339, UTC (суффиксZ), например2006-01-02T15:04:05Z(секундная точность). Фиксированная ширина сохраняет лексикографическую сортировку TEXT, то есть хронологию (ORDER BY created_at). Единая точка генерации — приложение, а не умолчание в схеме: так забытая вставка падает громко. Измерение длительности — не метка времени. - Миграции — goose (
migrations/): SQL-файлы для DDL; Go-миграции (goose.AddMigrationContext) — когда нужен код (генерация id, заполнение задним числом). При изменении структуры обновляем схему ../database.md тем же изменением. - Добавляя колонку, соблюдай инвариант «Новая колонка правится во всех четырёх местах» — CLAUDE.md, «Инварианты».