Роадмап: web-access переименована под приложение, которое ставится на телефон; заведена цель ready-notification — уведомление о готовности без открытого приложения. Беклог: десять задач. Многопользовательская цепочка (oidc-login, record-ownership, telegram-account-link), веб (json-api-for-spa, spa-skeleton, upload-and-status-screen, records-list-screen, installable-pwa), уведомления через apprise и ntfy, разведка выбора фреймворка. Очередь: долги, хранилище, вход, приложение. Решение сменилось с htmx на SPA, поэтому conventions/web-ui.md снята целиком и оставлена честной строкой до итога разведки. Открытые вопросы архитектуры, границы паспорта и триггеры метки ревью приведены в соответствие.
70 lines
5.7 KiB
Markdown
70 lines
5.7 KiB
Markdown
# Конвенции кода
|
||
|
||
Как мы пишем код — в отличие от `openspec/specs/`, который описывает, что
|
||
система делает, и от [../architecture.md](../architecture.md), который описывает,
|
||
как она сложена.
|
||
|
||
**Прозой остаётся только то, что не выражается правилом.** Свойство, ставшее
|
||
правилом линтера или тестом-сканером, отсюда **удаляется** и переезжает в
|
||
перечень «Механизировано» ниже. Причина: файл на несколько сотен строк
|
||
размазывает внимание по тривиальному — и модель, и человек добросовестно
|
||
проверят именование и не дойдут до формы решения.
|
||
|
||
Обоснование «почему именно так» живёт в [../adr/](../adr/README.md); инварианты с
|
||
severity — в [CLAUDE.md](../../CLAUDE.md).
|
||
|
||
## Откуда взяты и что с расхождениями
|
||
|
||
Четыре записи перенесены из проекта jellybit — тот же Go, тот же автор, те же
|
||
задачи. Код transcriber написан раньше и **части правил не следует**: ключи —
|
||
UUID вместо ULID, время берётся `time.Now()` по месту, лог пишется на каждом
|
||
шаге и дублируется воркером, доменные ошибки проверяются приведением типа.
|
||
|
||
Пятая, `web-ui.md`, тоже пришла оттуда, но не прижилась: jellybit работает на
|
||
htmx, а здесь решено делать SPA — и перенесённый текст снят целиком.
|
||
|
||
Каждое такое место названо в своей записи строкой «*Расхождение:*». Читается оно
|
||
как **долг, а не как нарушение**: правила действуют на новый код, переписывание
|
||
существующего — отдельная работа. Проходу ревью строка «Расхождение» говорит, что
|
||
находка на этом месте уже известна и новой не считается.
|
||
|
||
## Записи
|
||
|
||
- [logging.md](logging.md) — логирование: уровень по адресату, единая логирующая
|
||
точка на доменной границе, словарь полей, `ext.*`, что не логируем.
|
||
- [errors.md](errors.md) — ошибки: stdlib, обёртка `%w`, `errors.Is` и
|
||
`errors.As`, трансляция доменной ошибки на внешней границе, sentinel против
|
||
типизированной.
|
||
- [config.md](config.md) — конфигурация: TOML, секреты рендерит выкладка в файл
|
||
`0600`, самодокументируемый `config.dist.toml`, проверка на старте.
|
||
- [database.md](database.md) — БД и идентификаторы: время в UTC RFC 3339, TEXT
|
||
ULID, разбор на входной границе, естественные ключи у деталей.
|
||
- [web-ui.md](web-ui.md) — веб-UI: что решено про приложение (SPA, установка на
|
||
телефон, статика в бинарнике) и что ждёт выбора фреймворка. **Почти пуста:**
|
||
редакция на htmx снята 2026-08-10 вместе со сменой решения, а новую писать не
|
||
под что до разведки `spa-framework-choice`.
|
||
|
||
## Механизировано
|
||
|
||
Проверяется командами из [CLAUDE.md](../../CLAUDE.md); прозой не дублируется и в
|
||
промптах ревью не пересказывается.
|
||
|
||
| Правило | Где механизировано |
|
||
| --- | --- |
|
||
| Сравнение ошибок через `errors.Is` и `errors.As`, не `==` и не приведением типа | `.golangci.yml` → `errorlint` |
|
||
| Непроверенное возвращаемое значение ошибки | `.golangci.yml` → `errcheck` (кроме `defer Close` и `send`) |
|
||
| Форматирование исходников | `.golangci.yml` → `gofmt` |
|
||
| Подозрительные конструкции языка | `.golangci.yml` → `govet`, `staticcheck`, `ineffassign`, `unused` |
|
||
| Секреты в коммите | `lefthook.yml` → `gitleaks git --staged` |
|
||
| Раскладка документов, битые ссылки, миграция без правки `database.md` | `docs.py check` |
|
||
|
||
Не названное здесь место механизации означает, что проход по конвенциям будет
|
||
добросовестно проверять уже проверенное.
|
||
|
||
**Из перечисленного в записях правилом выражено одно** — сравнение ошибок через
|
||
`errors.Is` и `errors.As` (`errorlint`, строка таблицы выше). Прозой остаётся всё
|
||
прочее: ни константный `msg` лога (`sloglint`), ни запрет `fmt.Print*` и
|
||
`os.Getenv` (`forbidigo`), ни запрет сторонних пакетов ошибок (`depguard`), ни
|
||
архитектурные тесты-сканеры. Это следующий шаг переноса в правило: свойство,
|
||
оставшееся прозой, проверяет человек на каждом ревью заново.
|