Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# The service's variables, with the values a developer's machine runs with.
# Every value here is also the one the service assumes when the variable is
# unset, except where a line says otherwise.
#
# Copy this file to .env and change what you need: .env is never committed.
# Nothing reads .env by itself — the service reads its environment only — so
# export it into the shell before `just run`:
#
# set -a; . ./.env; set +a; just run
#
# The debugger's configurations in .vscode/launch.json set their own values.

# The port the server listens on. Cloud Run sets it on every revision.
PORT=8080

# local or prod. Local switches on what development alone may do; Cloud Run,
# where K_SERVICE is set, refuses to start as local.
CANVAS_ENV=local

# debug, info, warn or error.
CANVAS_LOG_LEVEL=info

# json for a log collector, console to read by eye. The service assumes json
# when unset, and refuses console where K_SERVICE is set.
CANVAS_LOG_FORMAT=console

# How long the server waits for each part of a request, and how long it keeps
# an idle connection open.
CANVAS_HTTP_READ_HEADER_TIMEOUT=5s
CANVAS_HTTP_READ_TIMEOUT=30s
CANVAS_HTTP_WRITE_TIMEOUT=60s
CANVAS_HTTP_IDLE_TIMEOUT=120s

# The whole way out once a stop is asked for: three quarters for the requests
# in flight, a quarter for closing what the service ran on.
CANVAS_SHUTDOWN_TIMEOUT=9s
19 changes: 18 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,24 @@
"mode": "debug",
"program": "${workspaceFolder}/cmd/server",
"env": {
"PORT": "8080"
"PORT": "8080",
"CANVAS_ENV": "local",
"CANVAS_LOG_LEVEL": "debug",
"CANVAS_LOG_FORMAT": "console"
}
},
{
"name": "Run the server as Cloud Run sees it",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/cmd/server",
"env": {
"PORT": "8080",
"K_SERVICE": "canvas-api",
"CANVAS_ENV": "prod",
"CANVAS_LOG_LEVEL": "info",
"CANVAS_LOG_FORMAT": "json"
}
},
{
Expand Down
26 changes: 25 additions & 1 deletion RUN.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@
|---|---|---|---|---|---|
| [x] | T61 | Документы: журнал решений и CLAUDE.md | Claude, проверяю я | — | — |
| [x] | T62 | Документы: product, privacy, spec, stack | Claude, проверяю я | — | T61 |
| [ ] | T16 | Каркас сервиса | Claude | — | T61 |
| [x] | T16 | Каркас сервиса | Claude | — | T61 |
| [ ] | T63 | Terraform: провайдеры 8.7.0 | Claude | — | — |
| [ ] | T64 | Облако E2: секрет ключа, Agent Platform, бюджет | Claude | да | T63 |
| [ ] | T65 | Доступ: CORS и ключ | Claude | да | T16, T64 |
Expand Down Expand Up @@ -546,6 +546,30 @@
- строки лога — JSON с `severity`;
- после мержа CD выкатывает каркас, `/health` отвечает релизом и коммитом.
- **Не делать:** телеметрию — E15 (черновик T17); protobuf — T18; CORS и ключ — T65.
- **Решено автором (2026-10-16):** `CANVAS_ENV` по умолчанию `local`, при `K_SERVICE` — только `prod`, и Terraform ставит его; в spec 8.3 добавлены `INTERNAL_ERROR` (500) и `METHOD_NOT_ALLOWED` (405) — их отвечают паника и неразрешённый метод.
- **Сделано (2026-10-16):** образец — `mathtrail-standalone` на коммите c6cbeb2, склонированный только для чтения: в облачной сессии нет `reference/`.
- **Перенесено из образца:**
- `cmd/server/main.go` — `signalled`, `run`, `exitCode`, ошибка, которую записал лог, печатается один раз, остановка во время старта — выход 0, `errgroup`. Строка `startup` пишется zap и называет `version`, `commit`, `date`, `port`, `env`, `deployed`. В отличие от образца порт занимается до неё: занятый порт — сбой старта, а не `shutdown`;
- `internal/app` — `server.go` целиком: тайм-ауты, drain — 3/4 пути остановки, обрыв запросов по сроку — предупреждение, а не сбой. `container.go` — `Config`, `Logger`, `Router` и закрытие в обратном порядке; сборка идёт списком шагов, и сбой шага закрывает уже построенное на отдельном контексте;
- `internal/config` — viper, только объявленные ключи, пустое значение — незаданное, `Validate()` называет переменную. Переменные — `PORT`, `CANVAS_ENV`, `CANVAS_LOG_LEVEL`, `CANVAS_LOG_FORMAT`, `CANVAS_HTTP_*_TIMEOUT`, `CANVAS_SHUTDOWN_TIMEOUT`, `K_SERVICE`;
- `internal/logger` — без изменений, кроме модуля и примеров в тестах;
- `internal/apierror` — `{code, message}` и `details` с `omitempty`; коды `NOT_FOUND`, `METHOD_NOT_ALLOWED`, `INTERNAL_ERROR`. Остальные коды E2 придут с T18, T65 и T70;
- `internal/transport/http` на gin — роутер, `/health` на GET и HEAD, 404 и 405 телом `apierror`; middleware в порядке образца: `LastResort`, `SecurityHeaders`, `RequestID`, `ZapLogger`, `ZapRecovery`.
- **Отличия от образца:** нет телеметрии, метрик, лимитов, проверки `Host` и `Origin`, полей query в логе и продуктовых маршрутов; заголовок — `X-Request-Id`, как в spec 8.1, и id всегда свой: сервер не читает id клиента, метод пишет одним из слов закрытого набора, а маршрут — шаблоном из таблицы самого роутера (R68); `CANVAS_ENV` и его защита при `K_SERVICE` — своё; при `K_SERVICE` `CANVAS_SHUTDOWN_TIMEOUT` меньше 10 с — Cloud Run убивает экземпляр через 10 с после SIGTERM. Косвенные модули подняты до версий образца.
- **Облако:** `CANVAS_ENV=prod` в `service_api.tf` и тест `terraform test` на него.
- **Ещё:** `.env.example`, `.vscode/launch.json` — локальный запуск и «как видит Cloud Run», `THIRD_PARTY_LICENSES`, spec 8.3 и 15.4.
- **Проверки** (облачная сессия; `just`, golangci-lint 2.14.0 и Terraform 1.16.3 скачаны по закреплённым версиям): зелёные — и повторно после R68 — `ci-lint`, `ci-test` (покрытие 97,6 %; после R68 тест метода обходит весь закрытый набор), `ci-mocks-check`, `ci-secrets`, `release-version`, `release-artifacts`, `terraform fmt -check`. Вживую: `/health` отвечает версией и коммитом, 404 и 405 — телом `apierror`, каждый ответ несёт `X-Request-Id`, строки лога — JSON с `severity`; с `K_SERVICE` и без `CANVAS_ENV=prod` сервис не стартует.
- **Выключены, чтобы доказать тесты, — 19 проверок, каждая краснила свой тест:** `local` и `console` при `K_SERVICE`, неизвестный `CANVAS_ENV`, предел `CANVAS_SHUTDOWN_TIMEOUT` в развёртывании, имя переменной в точном регистре, `PORT` только цифрами, закрытие при сбое сборки, отдельный контекст закрытия, обратный порядок, `RequestID` в цепочке, `HEAD /health`, 405, порт до строки `startup`, сброс заголовков обработчика при панике, id клиента не читается, метод только из закрытого набора, форма запасного id, маршрут из таблицы роутера, пропуск проб в логе.
- **Fuzz:** нет. Был `FuzzRequestID` на заголовок `X-Request-Id` — 60 с без находок; он ушёл вместе с чтением заголовка (R68), и внешнего ввода каркас больше не разбирает.
- **PR #20, находки CodeQL `go/log-injection`** — четыре, на строке лога запроса и в `LastResort`: в них шли метод и id клиента. Id клиента не читается, метод пишется `switch` по закрытому набору (R68). Ревью правки — 8 находок: исправлено 7 — таблица-тождество методов заменена `switch`, а R68 говорит, что это не защита, а запись закрытого набора; общая проверка формы id получила предел длины; убраны тест-дубль уникальности id, случай «длиннее id» и неиспользуемый помощник; в RUN.md обновлены проверки; дата R68 названа явно. Отброшено 1: форму id в тесте роутера проверяет тест middleware. Второй круг — 6 находок: исправлено 5 — тесты сверяют id как UUID v7, а запасной — по своему узору, вместо общего широкого правила в двух копиях; таблица случаев сокращена до двух; тест роутера тоже требует UUID v7; `switch` метода и форма запасного id доказаны выключением. Отброшено 1: тест метода нарочно перечисляет набор отдельно от кода — так он и ловит расхождение. После этого CodeQL на коммите d9574eb оставил те же 4 находки: в модели gin у CodeQL источником помечен и `Context.FullPath`, из которого шёл маршрут. По решению автора маршрут пишется шаблоном из таблицы роутера (`middleware.Routes`). Ревью этой правки, два круга. Первый — 6 находок: исправлено 2 — фильтр проб берёт маршрут из той же таблицы, устаревший комментарий поправлен; отброшено 3 — таблица чужого роутера (ошибка вызова, видная в коде), таблица-тождество (решение автора), ссылка на роутер; 1 — поздняя регистрация маршрута — сначала исправлено перечитыванием таблицы, но второй круг напомнил, что gin не берёт маршруты, пока обслуживает, и перечитывание убрано: таблица читается один раз, при первом запросе. Второй — 6 находок: исправлено 4 этим возвратом; отброшено 2 — у `NewRoutes` нет интерфейса, который он вернул бы, а закрывает ли таблица находки CodeQL, показывает CI на PR.
- **Ревью `/code-review high`, два круга.**
- Первый — 9 находок. Исправлено 6: `.env.example` обещал, что `.env` прочтут, — теперь сказано, как его загрузить; занятый порт писался как сбой `shutdown` после строки `startup` — порт теперь занимается до неё; тесты `run()` зависели от переменных оболочки — они их обнуляют; `CANVAS_SHUTDOWN_TIMEOUT` в развёртывании ограничен 10 с; повтор перехвата stdout в тесте заменён помощником; нет fuzz на `X-Request-Id` — добавлен. Отброшено 3: верхняя граница теста drain — тест о самих часах ставит свои сроки, как и в образце; 5-секундные сторожа ожиданий — предел для событий, которые занимают миллисекунды, а не мера скорости кода; список шагов контейнера — его требует задача (закрытие при сбое посередине), следующий ресурс принесёт T35.
- Второй — 8 находок. Исправлено 5: ответ на панику нёс заголовки обработчика; имена переменных сравнивались без учёта регистра; `PORT` принимал `+8080` и `08080`; комментарий `Listen` называл вызов только тестовым; у четырёх полей тайм-аутов не было doc-комментариев. Отброшено 1: список шагов контейнера — повтор находки первого круга. В «Открыто» — 2, ниже.
- **Открыто:**
- `http.Server.ErrorLog` не задан, как и в образце: свои ошибки net/http пишет в stderr простым текстом, без `severity`, и в них бывает адрес клиента. Предлагаю `zap.NewStdLog` с правилом для адреса в задаче, которая вводит таблицу событий лога (T70);
- `freePort` в тестах `main` освобождает порт до того, как `run` его займёт, как и в образце: другой процесс может занять его в этом окне. Флейка не было; лечится, если `run` начнёт принимать готовый listener;
- не запускались — сеть сессии их не пускает: `tf-check` (реестр Terraform), `ci-vuln` (vuln.go.dev), сверка `ci-licenses` со списком (ссылки на тексты лицензий пяти модулей с vanity-адресами взяты из списка образца — версии те же); без демона Docker — `ci-image-smoke`, `ci-image-scan`, `ci-workflows`. Их прогонит CI на PR;
- описания правил depguard в `.golangci.yml` ещё называют порты `Recognizer` и `Tutor` — поправит T35 вместе с портом (долг Д23).
- **Разделы:** spec 8.1–8.3, 15.4, 16, 18; R11, R30; standalone: R18, R19, R74, R77.

#### T63. Terraform: провайдеры 8.7.0
Expand Down
33 changes: 33 additions & 0 deletions THIRD_PARTY_LICENSES
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,39 @@ builds them, listed as std. Each line is a license, what it covers, and the
text of that license at the exact version that is built.

BSD-3-Clause std https://github.com/golang/go/blob/go1.27.2/LICENSE
BSD-3-Clause github.com/fsnotify/fsnotify https://github.com/fsnotify/fsnotify/blob/v1.9.0/LICENSE
MIT github.com/gabriel-vasile/mimetype https://github.com/gabriel-vasile/mimetype/blob/v1.4.15/LICENSE
MIT github.com/gin-contrib/sse https://github.com/gin-contrib/sse/blob/v1.1.2/LICENSE
MIT github.com/gin-gonic/gin https://github.com/gin-gonic/gin/blob/v1.12.0/LICENSE
MIT github.com/go-playground/locales https://github.com/go-playground/locales/blob/v0.14.2/LICENSE
MIT github.com/go-playground/universal-translator https://github.com/go-playground/universal-translator/blob/v0.18.2/LICENSE
MIT github.com/go-playground/validator/v10 https://github.com/go-playground/validator/blob/v10.30.5/LICENSE
MIT github.com/go-viper/mapstructure/v2 https://github.com/go-viper/mapstructure/blob/v2.5.0/LICENSE
MIT github.com/goccy/go-yaml https://github.com/goccy/go-yaml/blob/v1.19.2/LICENSE
BSD-3-Clause github.com/google/uuid https://github.com/google/uuid/blob/v1.6.0/LICENSE
MIT github.com/leodido/go-urn https://github.com/leodido/go-urn/blob/v1.5.0/LICENSE
MIT github.com/mattn/go-isatty https://github.com/mattn/go-isatty/blob/v0.0.24/LICENSE
MIT github.com/pelletier/go-toml/v2 https://github.com/pelletier/go-toml/blob/v2.4.3/LICENSE
MIT github.com/quic-go/qpack https://github.com/quic-go/qpack/blob/v0.6.0/LICENSE.md
MIT github.com/quic-go/quic-go https://github.com/quic-go/quic-go/blob/v0.63.0/LICENSE
MIT github.com/sagikazarmark/locafero https://github.com/sagikazarmark/locafero/blob/v0.11.0/LICENSE
MIT github.com/sourcegraph/conc https://github.com/sourcegraph/conc/blob/5f936abd7ae8/LICENSE
Apache-2.0 github.com/spf13/afero https://github.com/spf13/afero/blob/v1.15.0/LICENSE.txt
MIT github.com/spf13/cast https://github.com/spf13/cast/blob/v1.10.0/LICENSE
BSD-3-Clause github.com/spf13/pflag https://github.com/spf13/pflag/blob/v1.0.10/LICENSE
MIT github.com/spf13/viper https://github.com/spf13/viper/blob/v1.21.0/LICENSE
MIT github.com/subosito/gotenv https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE
MIT github.com/ugorji/go/codec https://github.com/ugorji/go/blob/codec/v1.3.2/codec/LICENSE
Apache-2.0 go.mongodb.org/mongo-driver/v2 https://github.com/mongodb/mongo-go-driver/blob/v2.9.1/LICENSE
MIT go.uber.org/multierr https://github.com/uber-go/multierr/blob/v1.10.0/LICENSE.txt
MIT go.uber.org/zap https://github.com/uber-go/zap/blob/v1.28.0/LICENSE
MIT go.yaml.in/yaml/v3 https://github.com/yaml/go-yaml/blob/v3.0.5/LICENSE
BSD-3-Clause golang.org/x/crypto https://cs.opensource.google/go/x/crypto/+/v0.57.0:LICENSE
BSD-3-Clause golang.org/x/net https://cs.opensource.google/go/x/net/+/v0.60.0:LICENSE
BSD-3-Clause golang.org/x/sync/errgroup https://cs.opensource.google/go/x/sync/+/v0.23.0:LICENSE
BSD-3-Clause golang.org/x/sys https://cs.opensource.google/go/x/sys/+/v0.48.0:LICENSE
BSD-3-Clause golang.org/x/text https://cs.opensource.google/go/x/text/+/v0.42.0:LICENSE
BSD-3-Clause google.golang.org/protobuf https://github.com/protocolbuffers/protobuf-go/blob/v1.36.12/LICENSE

The npm packages below are what the SPA is built from: every package its
lockfile needs outside development, and every package one of those names
Expand Down
Loading