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
6 changes: 6 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"claudeMdExcludes": [
"**/reference/mathtrail-standalone/**/CLAUDE.md",
"**/reference/mathtrail-standalone/**/CLAUDE.local.md"
]
}
3 changes: 3 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
// gopls and dlv come pinned from the image; the Go extension must not fetch its own.
"go.toolsManagement.autoUpdate": false,
"go.toolsManagement.checkForUpdates": "off",
// The sample under reference/ is a read-only submodule: Source Control does
// not offer it as a second repository to commit in.
"git.detectSubmodules": false,
"files.eol": "\n",
"files.insertFinalNewline": true,
// SonarQube for IDE sends nothing about how it is used.
Expand Down
16 changes: 16 additions & 0 deletions .devcontainer/post-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ if [[ -d .githooks ]]; then
git config core.hooksPath .githooks
fi

# A pull or a checkout that moves the sample's pin moves the sample with it. Otherwise it
# stays at the old commit, reads as the pinned one, and the next "commit all" takes the
# pin back.
git config submodule.recurse true

# The sample this repository follows is a submodule, and a fresh clone leaves it empty.
# Checked out only when it is missing: once it is there, it may sit at a commit somebody
# has just raised it to and not committed yet, and a restart must not take that back.
# Without the network the container still starts; the next start tries again. A clone
# that hangs, or a remote that asks for credentials, is cut short rather than holding
# the start.
if [[ ! -e reference/mathtrail-standalone/.git ]]; then
GIT_TERMINAL_PROMPT=0 timeout 120 git submodule update --init reference/mathtrail-standalone \
|| echo "reference/mathtrail-standalone: not checked out; run git submodule update --init" >&2
fi

# The docker inside this container hands its networks an MTU of 1500. When this container's
# own interface is smaller — a tunnel on the host is the usual reason — everything that
# crosses both stalls: a packet that fits here is too large for the tunnel, and nothing
Expand Down
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
**/.env.*
.devcontainer
reference
.gitmodules
.ignore
docs
draft
web
Expand Down
2 changes: 1 addition & 1 deletion .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# not become a wait.
set -euo pipefail

# The repository's own Go files: the copy of the sample under reference/ is
# The repository's own Go files: the sample under reference/ is
# read-only and not this repository's to answer for, and installed packages and
# providers are not its own either. The same list as GO_FILES in the justfile,
# which this hook does not read, since a commit may be made where there is no
Expand Down
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ firebase-debug.*.log*
# permission of a live run, which is deleted once the run is over.
.claude/settings.local.json

# Reference copies of other repositories: read-only material, never committed.
/reference/
# Other repositories' material for reading. The sample is a submodule and is tracked as
# one; anything else put here stays out of this public repository.
/reference/*
!/reference/mathtrail-standalone

# The one deployment this repository delivers to is described in the repository,
# not in somebody's console: these two are committed on purpose.
Expand Down
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "reference/mathtrail-standalone"]
path = reference/mathtrail-standalone
url = https://github.com/MathTrail/mathtrail-standalone.git
branch = main
5 changes: 5 additions & 0 deletions .ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Search tools that read this file — ripgrep, and the editors and agents built on it —
# skip the sample under reference/: git tracks it as a submodule, but its files are
# another repository's, and a search here is about this one. An explicit path still
# reads them.
/reference/
34 changes: 12 additions & 22 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Project context lives in these files, not in chat history.
- Before starting, read the RUN.md task, the product, spec and privacy sections it cites, and `docs/decisions.md`; a DevX task also reads the files of the sample it names under `reference/mathtrail-standalone/`. If the task contradicts them or something is missing, stop and ask. Do not silently fill gaps; record them as open questions under the next free number, which the section's opening names — О- in product 15 (the open ones are in 15.2), Т- in spec 20, П- in privacy 12 — or in the «Замечания» section of the document being written.
- Before calling any change done — code, config, docs or content — run `/code-review high` on it. Skip it only when the author asks, for that change alone, and say in the report that it did not run.
- Run it once the change is written and its checks — `just ci-lint`, `just ci-test` and `just ci-mocks-check` (from T04), `just ci-licenses` and `just ci-vuln` (from T05), `just ci-secrets`, `just ci-image-smoke` and `just ci-image-scan` (from T06), `just release-version` and `just release-artifacts` (from T08), `just tf-check` (from T10), `just ci-workflows` (from T11), the others RUN.md rule 7 names for what the change touches, whatever else the task names — are green.
- The checks and the review run in a clean copy of the tree that holds this change and nothing else, so that another session's uncommitted work gets into neither. A tree that holds nothing but the change is that copy already. Otherwise the copy is a `git worktree` of `HEAD`, outside the repository, given this change's own edits, new files included, and what its checks need that git does not keep — a link to `reference/`, `web/node_modules` installed from the lockfile — and the checks and the review run from inside it. Fixes are made in the tree and carried into the copy, and `git worktree remove --force` takes the copy away after the review: it holds nothing the tree does not.
- The checks and the review run in a clean copy of the tree that holds this change and nothing else, so that another session's uncommitted work gets into neither. A tree that holds nothing but the change is that copy already. Otherwise the copy is a `git worktree` of `HEAD`, outside the repository, given this change's own edits, new files included, and what its checks need that a worktree does not bring — `web/node_modules` installed from the lockfile; the checks do not read the sample, as CI, which checks out no submodule, shows, so a review that needs it reads it in the tree — and the checks and the review run from inside it. Fixes are made in the tree and carried into the copy, and `git worktree remove --force` takes the copy away after the review: it holds nothing the tree does not.
- It reads the working tree plus the branch's commits — those not yet pushed, or all since `main` without an upstream — but not untracked files. Mark new files with `git add -N` for the review and unmark them with `git reset -- <file>` afterwards: the mark makes `git stash` fail. Pass no path — a path is reviewed instead of the diff, not beside it.
- Verify each finding against the repository before acting on it; do not use `--fix`. Fix a real defect your uncommitted work introduced, wherever it shows up, with a test that fails without the fix when the defect is in code. A defect your work did not introduce — in an earlier commit, in code your work does not touch, or in another session's uncommitted work in the same tree — goes to the report as open.
- Drop a wrong finding with a one-line reason, and one that re-proposes a rejected alternative with no new evidence by naming the decision. A finding that questions product, spec, privacy or a decision for a reason that holds goes to the author as a question.
Expand All @@ -87,7 +87,7 @@ Project context lives in these files, not in chat history.

## Hard rules

- **Devcontainer only.** All work happens inside the devcontainer (`.devcontainer/`, from T03): building, tests, linters, mocks, running the server and the SPA, Terraform, gcloud and live runs. Nothing but Docker and VS Code is installed or run on the host. If you are not inside the container (`MATHTRAIL_DEVCONTAINER` is not `1`), stop and ask the author to "Reopen in Container". "The change is only documents" is no reason to go on: stop before the first edit. The one exception is a cloud session of Claude Code that the author started for the task: it runs there without the container, runs the checks it can reach, and the report names every check that did not run. The exceptions run on the host: T00–T03, the only host-side tasks (T03 creates the container), and the author's refresh of the reference copy (see "Reference copies").
- **Devcontainer only.** All work happens inside the devcontainer (`.devcontainer/`, from T03): building, tests, linters, mocks, running the server and the SPA, Terraform, gcloud and live runs. Nothing but Docker and VS Code is installed or run on the host. If you are not inside the container (`MATHTRAIL_DEVCONTAINER` is not `1`), stop and ask the author to "Reopen in Container". "The change is only documents" is no reason to go on: stop before the first edit. The one exception is a cloud session of Claude Code that the author started for the task: it runs there without the container, runs the checks it can reach, and the report names every check that did not run. The exceptions run on the host: T00–T03, the only host-side tasks (T03 creates the container).
- **Exact versions only.** No `latest`, no floating tags or ranges (spec 1). This covers:
- Docker images and devcontainer features: exact tag plus digest. An image whose tags name no version, such as distroless, is pinned by digest alone;
- Go and its modules: exact versions in `go.mod`;
Expand Down Expand Up @@ -282,24 +282,14 @@ Implementations of the same interface must be interchangeable, and that is worth

## Reference copies

`reference/mathtrail-standalone/` is a local, gitignored snapshot of the working tree of `mathtrail-standalone`, the sibling repository these rules and the DevX come from. The devcontainer cannot see a sibling directory of the host, so a task reads the snapshot instead.

- Read-only: never edit it, and never commit anything from it.
- It is what R11 ports from: the skeleton (`app`, `config`, `logger`, `telemetry`, `apierror`, `version`), the devcontainer, the justfile, CI/CD, Terraform and the quality settings — each changed only where canvas-api needs it. "As in standalone" in RUN.md means: take the named file from the snapshot and change only what the task names.
- Its rules file is renamed to `CLAUDE.standalone.md`. Under its own name, Claude Code would load it whenever a file of the snapshot is read, and its rules do not apply here: it is the sample this file follows, section by section.
- Left out of it: the sample's `.git`, its installed packages, binaries, coverage profile and Terraform providers (`node_modules`, `bin/`, `dist/`, `coverage.out`, `.terraform/`), its own reference material (`prototype/`, `reference/`, `research/`, `draft/`) — so mentor-api, which the sample keeps under `reference/`, is not here — and what is local to the sample's machine: a `.env` with its secrets and a `CLAUDE.local.md`, which Claude Code would load just like a `CLAUDE.md`.
- Search tools that respect `.gitignore` (ripgrep) skip it. Use explicit paths (`Read`, `ls`, `grep -r reference/mathtrail-standalone/...`).
- The author refreshes it on the host, from the root of this repository:

```sh
mkdir -p reference && rsync -a --delete \
--exclude=.git --exclude=node_modules \
--exclude=/prototype/ --exclude=/reference/ --exclude=/research/ --exclude=/draft/ \
--exclude=bin/ --exclude=dist/ --exclude=coverage.out --exclude=.terraform/ \
--exclude=.env --exclude=CLAUDE.local.md \
../mathtrail-standalone/ reference/mathtrail-standalone/ \
&& mv reference/mathtrail-standalone/CLAUDE.md reference/mathtrail-standalone/CLAUDE.standalone.md
```
`reference/mathtrail-standalone/` is a git submodule of [mathtrail-standalone](https://github.com/MathTrail/mathtrail-standalone), the sibling repository these rules and the DevX come from, pinned to one commit of it. `post-start` checks it out when a fresh clone left it empty, and sets `submodule.recurse`, so a pull or a checkout that moves the pin moves the sample too; elsewhere, `git submodule update --init` does both.

- **Its rules do not apply here.** Every `CLAUDE.md` under `reference/` — the sample's own, and any a later commit of it brings — holds another repository's rules, and binds nothing in this repository. `claudeMdExcludes` in `.claude/settings.json` keeps Claude Code from loading them; should one reach you anyway, ignore it. This file is the only one that does, and the sample is what it follows, section by section.
- Read-only: never edit it, and never commit inside it.
- It is what R11 ports from: the skeleton (`app`, `config`, `logger`, `telemetry`, `apierror`, `version`), the devcontainer, the justfile, CI/CD, Terraform and the quality settings — each changed only where canvas-api needs it. "As in standalone" in RUN.md means: take the named file from the sample and change only what the task names.
- It holds what the sample's git tracks, and so none of what that repository keeps out of git: its installed packages, binaries, a `.env` with its secrets, a `CLAUDE.local.md`. mentor-api, which the sample keeps under a `reference/` of its own outside git, is not here.
- `.ignore` keeps search tools built on ripgrep out of it, and `sonar.exclusions` keeps the analysis out, so a search or a finding here is about this repository. Use explicit paths (`Read`, `ls`, `grep -r reference/mathtrail-standalone/...`).
- To raise the pin, run `git submodule update --remote reference/mathtrail-standalone` in the container; it moves the sample to the head of its `main`, the branch `.gitmodules` names. The author commits the new pin like any other change.

## Environment

Expand All @@ -324,8 +314,8 @@ Inside the container:
- The everyday ones are `just fmt`, `just test`, `just lint`, `just build` and `just run`, and for the SPA `just web-dev`, which serves it on port 5173, `just web-fmt` and `just web-test`, and for the cloud `just tf-fmt` and `just usage`, which sets what the deployed service used of Cloud Run against a month's free tier. `just web-build` builds the SPA into `web/dist`; `just build` and the image leave it out (R10).
- The ones that must be green before a task is done are **`just ci-lint`** (formatting and lint), **`just ci-test`** (the race detector and coverage), **`just ci-mocks-check`**, **`just ci-licenses`**, **`just ci-vuln`**, **`just ci-secrets`** (gitleaks over the whole history and over what is not committed yet), **`just ci-image-smoke`** (the runtime image starts, and its `/health` names this build), **`just ci-image-scan`** (Trivy over that image, HIGH and CRITICAL with a fix out), **`just release-version`** (the version the next release takes, from `RELEASE_LINE` in `release.yml` and the tags), **`just release-artifacts`** (what a release holds: the server for amd64 and arm64, the SPA's archive and `SHA256SUMS`, in `dist/`), **`just tf-check`** (the Terraform files formatted, the configuration valid against the providers its lock file names, and its tests on mock providers, with no credential) and **`just ci-workflows`** (actionlint, with shellcheck over the scripts of the workflows' steps but not of the composite actions': what GitHub would refuse or misread in the workflows, the call from `release.yml` to `deploy.yml` among it), and they are what CI runs (`.github/workflows/ci.yml`). `ci-licenses` holds every Go module and npm package to the license list and `THIRD_PARTY_LICENSES` to the dependency graph — `just licenses` rewrites it. `ci-vuln` runs govulncheck and `npm audit` (R48). A false alarm of the secret scan is waived in `.gitleaks.toml`, by a rule, a path and the shape of a value together, a vulnerability only in `.trivyignore`, and a finding of actionlint only in `.github/actionlint.yaml`; each with its reason.
- A change to `web/` must also pass **`just ci-web`**: Biome, the types, the tests with their coverage, and a build.
- gofmt never walks into `reference/`: the copy is the sample's, read-only.
- gofmt never walks into `reference/`: the sample there is another repository's, read-only.
- **Git hooks.** `.githooks/pre-commit` is enabled by `post-start` through `core.hooksPath`. It runs formatting, a build and the tests — enough to catch what is embarrassing, while the linter, the race detector, `govulncheck` and `gitleaks` wait for CI. Where no Go toolchain is reachable — a Git client outside the container, for instance — it says so and skips those checks instead of blocking the commit: CI runs them again and is what gates a merge.
- **The runtime image.** `just docker-build` builds it and `just docker-run` starts it on port 8080. Both images in the root `Dockerfile` are pinned: a Go builder by tag and digest, at the version `go.mod` names, and a distroless static runtime by digest, since distroless names no versions in its tags; the runtime has no shell and runs as a non-root user. The SPA is no part of it: Firebase Hosting serves the build (R10).
- **Build context.** Both images are built with the repository root as context: the devcontainer copies nothing from it, the runtime image the Go module. `.dockerignore` keeps out of it the history, every `.env` and its variants such as `.env.local`, `.github/`, `.vscode/`, `.sonarlint/`, `.claude/`, `.githooks/`, `.devcontainer/`, `reference/`, `docs/`, `draft/`, `web/`, `infra/`, every `testdata/`, `tools/`, `bin/`, `dist/`, test and profile output, `.scannerwork/`, a local `go.work`, `node_modules`, the Markdown files at the root, `firebase.json` and what the Firebase CLI leaves behind, and the credentials file Google's sign-in action writes into a workflow's checkout.
- **Build context.** Both images are built with the repository root as context: the devcontainer copies nothing from it, the runtime image the Go module. `.dockerignore` keeps out of it the history, every `.env` and its variants such as `.env.local`, `.github/`, `.vscode/`, `.sonarlint/`, `.claude/`, `.githooks/`, `.devcontainer/`, `reference/` with `.gitmodules` and `.ignore`, `docs/`, `draft/`, `web/`, `infra/`, every `testdata/`, `tools/`, `bin/`, `dist/`, test and profile output, `.scannerwork/`, a local `go.work`, `node_modules`, the Markdown files at the root, `firebase.json` and what the Firebase CLI leaves behind, and the credentials file Google's sign-in action writes into a workflow's checkout.
- **The release image.** A delivery does not build the server again: the `release` stage of the same `Dockerfile` puts the server a release signed into the same runtime, from a build context of its own (`just ci-image-push`, R58).
2 changes: 1 addition & 1 deletion RUN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
>
> Этот файл задаёт порядок работы: маленькие задачи, которые запускаются по одной. После каждой задачи я проверяю результат и только потом запускаю следующую. Задачи собраны в эпики. Подробно расписан только текущий эпик, остальные расписываются, когда до них доходит очередь (R44).
>
> Практики, инструменты и настройки качества — те же, что в соседнем репозитории mathtrail-standalone. Его локальная копия — `reference/mathtrail-standalone/` (T01). «Как в standalone» значит: взять названный файл из копии и поменять в нём только то, что названо в задаче.
> Практики, инструменты и настройки качества — те же, что в соседнем репозитории mathtrail-standalone. Он подключён подмодулем git `reference/mathtrail-standalone/`, закреплённым на коммите образца (T01, R70). «Как в standalone» значит: взять названный файл из образца и поменять в нём только то, что названо в задаче.

## Как пользоваться

Expand Down
Loading