diff --git a/CLAUDE.md b/CLAUDE.md
index 5bc1519..411980d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -15,23 +15,25 @@ Clean Architecture, SOLID and KISS are the defaults, and they serve readability
## The project
-MathTrail Canvas (the name settled by R55, О-9) is a maths tutor for children in grades 1–6. The child solves a task by hand, with a stylus or a finger, on a squared draft sheet and writes the final answer in an answer box. On Check, an AI reads the answer, a program compares it with the task's key, and the child sees what was read. When the answer is wrong, the tutor looks at the draft, frames the place where the reasoning went wrong over the child's own strokes and asks one short guiding question. It never gives the answer. `canvas-api` is the name of the service, not of the product.
+MathTrail Canvas (the name settled by R55, О-9) is a maths tutor for children in grades 1–6. The child solves a task by hand, with a stylus or a finger, anywhere on a squared sheet — drawing, counting, making notes — and writes the final answer on the same sheet: there is no answer box (R61). On Send Answer, an AI finds and reads the final answer, a program compares it with the task's key, and the child sees what was read. When the answer is wrong, the tutor frames the place where the reasoning went wrong over the child's own strokes and asks one short guiding question. It never gives the answer. `canvas-api` is the name of the service, not of the product.
-The MVP is a closed, free beta for families (R05, R06). The parent owns the account and is always the subject of access; the child works on the parent's device, in child mode behind a PIN (R07). There are five static tasks from the mockup, kept in `tasks/` until a catalog exists (R40).
+The work goes in epics (R44). The current one, E2, is a minimal slice that measures Gemini before the beta is built (R60): on a computer, with a mouse or a graphics tablet's pen, since tablets come with the beta; six static tasks, one per grade 1–6, kept in `tasks/` until a catalog exists (R63); one Gemini call per check (R62); the author alone, behind an access key (R64); no database, sign-in, consent, OpenFGA or Centrifugo. Every check is measured — time, tokens, cost — and labelled on the author's panel, and `just eval` reruns the author's journal on other models (R65). What E2 finds decides how the beta checks.
-A false alarm is worse than no check: when the reading is unsure, the child is asked to rewrite the answer, and when a provider fails, the child gets a neutral line — never a mistake (product 5.6, R38).
+The MVP is the closed, free beta for families, epic E15 (R05, R06, R60). The parent owns the account and is always the subject of access; the child works on the parent's device, in child mode behind a PIN (R07).
-Technical shape (spec 1–2):
+A false alarm is worse than no check: when the reading is unsure, the child is asked to rewrite the answer or circle it, and when a provider fails, the child gets a neutral line — never a mistake (product 5.6, R38, R62).
-- one Go service, `canvas-api`, on Google Cloud Run, with OpenFGA as a sidecar on `localhost` (R09); managed services only — no Kubernetes, no queues (R01) — and the services scale to zero when idle;
-- Centrifugo v6, a Cloud Run service of its own, pushes events to the client over WebSocket and keeps the channels' history in Upstash Redis;
-- Neon Postgres holds the application's data and the OpenFGA store;
-- the parent signs in with Firebase Auth (Google or Apple), and every API call but `/health` and the CORS preflight also carries a Firebase App Check token (spec 8.1);
-- Gemini, on Gemini Enterprise Agent Platform (formerly Vertex AI), makes two calls: it reads the answer box's image on a Check — unless the daily limit is spent or the box is blank — and after a wrong answer it reads the draft's image together with the key and writes the frame and the question (R38, R39);
-- the SPA — React and Konva: a draft sheet with a drawing layer and a hint layer, and an answer box — lives in `web/` and is served by Firebase Hosting (R10, R13). The draft lives on the device, in IndexedDB; on Check the SPA sends PNGs drawn from the strokes — the answer box and, unless the sheet is empty, the draft — and the server keeps them only in the request's memory (R42);
+Technical shape (spec 1–2) — the beta's, with what E2 does otherwise:
+
+- one Go service, `canvas-api`, on Google Cloud Run, with OpenFGA as a sidecar on `localhost` (R09); managed services only — no Kubernetes, no queues (R01) — and the services scale to zero when idle. E2 runs the service alone, on one instance;
+- Centrifugo v6, a Cloud Run service of its own, pushes events to the client over WebSocket and keeps the channels' history in Upstash Redis. E2 has none: a check's whole result comes in its HTTP response (R23);
+- Neon Postgres holds the application's data and the OpenFGA store. E2 has no database: the server keeps nothing, and the author's journal lives on the author's device (R65);
+- the parent signs in with Firebase Auth (Google or Apple), and every API call but `/health` and the CORS preflight also carries a Firebase App Check token (spec 8.1). In E2 such a call carries the access key instead, which `ci-tf-apply` generates into Secret Manager and the author enters in the SPA once (R64);
+- Gemini, on Gemini Enterprise Agent Platform (formerly Vertex AI), reads the sheet. In E2 it makes one call per check — unless the daily ceiling is spent or the sheet is blank — that sees the task, its key and its solution, finds the final answer and reads it twice, as written and as a bare value, follows the reasoning step by step, frames the first mistake and asks the question (R62). Whether the beta keeps one call or goes back to two — reading the answer without the key, then analysing the draft with it (R38, R39) — is decided by what E2 measures;
+- the SPA — React and Konva: a squared sheet with a drawing layer and a hint layer, and no answer box — lives in `web/` and is served by Firebase Hosting (R10, R13). On Send Answer the SPA sends one PNG of the whole sheet, drawn from the strokes, and the server keeps it only in the request's memory (R42, R61); whether the beta sends the strokes instead is a research task of E2 (R66). In the beta the sheet lives on the device, in IndexedDB; in E2 only the author's journal keeps it;
- the API contract is protobuf, package `canvasapi.v1` in `proto/`, from which buf generates Go and TypeScript (R12);
-- a program decides every verdict: the reading is compared with the task's key, and a model only reads the answer and words the hint (spec 1, R37, R38);
-- the MVP has one environment, prod, and every merge to `main` ships to it once CI is green, from a signed release (R32, R33, R45).
+- a program decides every verdict: the reading is compared with the task's key, and a model only reads the sheet and words the hint. The model's own judgement of the answer, which E2 asks it for, never decides it (spec 1, R37, R38, R62);
+- there is one environment, prod, and every merge to `main` ships to it once CI is green, from a signed release (R32, R33, R45).
## Documents
@@ -85,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 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), and the author's refresh of the reference copy (see "Reference copies").
- **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`;
@@ -105,11 +107,11 @@ Project context lives in these files, not in chat history.
- **Children's data** (privacy 1, 4, 5).
- About the child we keep only a pseudonym, an avatar from the set and the grade — never a name, photo, birth date, school or contacts.
- Logs, metrics and traces never carry a direct identifier (email, name), the pseudonym, a check image, the read answer, hint text or a token; an ID in them is a pseudonymous one — a UUID or the parent's Firebase UID (spec 13, 16; privacy 4).
- - The child's strokes stay on the device (R42). The server holds the two check images only in the request's memory — never in the database, a log or a trace.
- - Gemini receives what privacy 5.1 lists and no more: on a Check that the limit or a blank box does not stop, the answer box's image and the answer's form; after a wrong answer, also the draft's image, the task's text and grade, its key and solution, the read answer, the matched trap, the hint's level and the sheet's earlier questions. We add no identifier, no pseudonym and no parent's data — though an image shows whatever the child wrote, a name included (privacy 5.3). Gemini's caching is switched off for the project, and none of its features that keep data is used (privacy 5.2).
- - Children's handwriting never enters the repository or `testdata/`: tests use synthetic data and adults' samples only (spec 17). Eval sets are built from adults' samples; children's samples join them only if П-10 allows.
- - No child's data exists before the operator has `verified` the parent's consent: until then, child profiles and canvases are refused with `CONSENT_REQUIRED` (R26).
-- **The answer stays hidden** until the task is solved. `GET /v1/tasks`, `POST /v1/canvases` and `GET /v1/canvases/{id}` return the task's open view, `TaskView`, without `answer` (spec 8.6, 13); the solution comes only with a `CORRECT` verdict. The key never goes into the reading call (R38) or a log line (spec 16). The draft analysis sees the key (R39), so every hint passes the leak check before it is shown; one that fails it is replaced by the author's text or a template (spec 6.3, 7.3).
+ - The child's strokes stay on the device (R42). The server holds the check image, one PNG of the whole sheet (R61), only in the request's memory — never in the database, a log or a trace.
+ - The service's calls to Gemini carry what privacy 5.1 and R62 list and no more. In E2, on a check that neither the daily ceiling nor a blank sheet stops, that is the sheet's image, the task's text and grade, its key and its solution (R62). So in E2 the key goes in with the reading, which the two-call scheme of R38 and R39 keeps it out of; which scheme the beta takes, E2 decides. We add no identifier, no pseudonym and no parent's data — though an image shows whatever the child wrote, a name included (privacy 5.3). Gemini's caching is switched off for the project, and none of its features that keep data is used (privacy 5.2).
+ - Children's handwriting never enters the repository or `testdata/`: tests use synthetic data and adults' samples only (spec 17). Eval sets are built from adults' samples; children's samples join them only if П-10 allows. An eval set is a processing of its own that goes through Gemini (privacy 4): `just eval` sends it the author's samples, and the research of R66 their strokes too. The author's samples of E2 — and the journal exports and eval reports made from them — live in `samples/`, outside the repository: the task that brings the journal adds it to `.gitignore` and `.dockerignore` (privacy 4, R65).
+ - No child's data exists before the operator has `verified` the parent's consent: until then, child profiles and canvases are refused with `CONSENT_REQUIRED` (R26). E2 has no children at all: only the author uses it, behind the access key (R60, R64).
+- **The answer stays hidden** until the task is solved. `GET /v1/tasks` — and, in the beta, `POST /v1/canvases` and `GET /v1/canvases/{id}` — return the task's open view, `TaskView`, without `answer` (spec 8.6, 13); the solution comes only with a `CORRECT` verdict. The key never goes into a log line (spec 16), nor, in the beta's two-call scheme, into the reading call (R38); in E2 the one call sees it (R62). A call that sees the key may leak it, so every question the model writes passes the leak check before it is shown; one that fails it is replaced by the author's text or a template (spec 6.3, 7.3). One exception, for E2 only: the check's response carries the model's whole analysis, which saw the key and may name it, to the author's panel (R65). In the beta it never reaches the child's screen.
- **Secrets** live only in Secret Manager, environment variables or a local `.env` (never committed). No keys, tokens or client secrets in the repository: `prod.auto.tfvars` and `backend.hcl` are committed and public (spec 15.2, T10). A value reaches Secret Manager through `ci-tf-apply` — from the repository's secrets, or generated by the recipe itself — never by hand after a merge (R34; RUN.md, rule 13).
- **Licenses.** The code and the tasks, their images included, are MIT. Only add dependencies with MIT-compatible licenses, and keep the third-party license list current.
@@ -122,19 +124,22 @@ These are the sample's standards, applied to canvas-api; R11 reuses the sample's
```
cmd/
server/ the canvas-api entry point
+ eval/ reruns the author's journal through the check on other models and settings; never in the server's image
migrate/ goose migrations, a Cloud Run job that runs before the service is updated
admin/ invites and consent verification, run by the operator as a Cloud Run job
cleanup/ the daily cleanup, a Cloud Run job
internal/
app/ config/ logger/ telemetry/ apierror/ version/ the skeleton, as in the sample (R11)
domain/ pure logic — no I/O, no SDKs, no transport
- canvas/ the sheet, coordinates, the check images, the frame's conversion
- answer/ normalization, the answer's form, comparison with the key, the verdict; the reading prompt
- tutor/ the route after a verdict, the hint ladder, the question's checks, templates; the analysis prompt
- check/ one check from start to end: lease, limits, calls and events through its own ports
+ canvas/ the sheet, coordinates, the check image, the frame's conversion
+ answer/ normalization, the answer's form, comparison with the key, the verdict
+ tutor/ the question's checks, the fallback text; in the beta, also the route after a verdict and the hint ladder
+ check/ one check from start to end through its own ports: the image, the ceiling, the model's call, the verdict
+ and the frame; in the beta, also the lease, limits and events
infra/ adapters to the outside world
- firebase/ openfga/ postgres/ gemini/ centrifugo/
- transport/http/ handlers; middleware in this order: CORS, App Check, ID token, limits
+ gemini/ firebase/ openfga/ postgres/ centrifugo/
+ transport/http/ handlers; middleware in this order: CORS, then the access key in E2 (R64);
+ CORS, App Check, ID token, limits in the beta
proto/canvasapi/v1/ the API contract (buf)
gen/ generated Go
tasks/ the static tasks: JSON, embedded with go:embed
@@ -151,25 +156,27 @@ draft/ the screen mockup: a proposal, not a spec
.devcontainer/ justfile .github/workflows/
```
-This is the layout of spec 18. The grouping — `domain/`, `infra/`, `transport/` — is mentor-api's and the sample's, without mentor-api's `repository.go` and `handler.go` inside each domain package, which make its domain import gin and pgx. Our domains are computation — the sheet's geometry and the check images, the answer's normalization and comparison, the tutor's rules — and they stay free of I/O. That is what lets them be tested with tables and properties alone, with no mocks at all (spec 17). Use cases that do I/O sit outside `domain/` for that reason, one package each: `check` runs a check, and deleting a profile or an account (R27), the daily cleanup and the flows behind the other endpoints get packages of their own beside it, named by the MVP epic as it builds them (spec 18). The HTTP router is Т-1's to settle before the MVP epic's first code; the sample and mentor-api use gin.
+This is the layout of spec 18. The grouping — `domain/`, `infra/`, `transport/` — is mentor-api's and the sample's, without mentor-api's `repository.go` and `handler.go` inside each domain package, which make its domain import gin and pgx. Our domains are computation — the sheet's geometry and the check image, the answer's normalization and comparison, the tutor's rules — and they stay free of I/O. That is what lets them be tested with tables and properties alone, with no mocks at all (spec 17). Use cases that do I/O sit outside `domain/` for that reason, one package each: `check` runs a check, and deleting a profile or an account (R27), the daily cleanup and the flows behind the other endpoints get packages of their own beside it, named by the beta's epic, E15, as it builds them (spec 18). The HTTP router is gin, as in the sample and mentor-api (R67).
+
+E2 builds only part of it: `cmd/server` and `cmd/eval`; the skeleton, all but `telemetry/`; and `domain/`, `check`, `infra/gemini`, `transport/http`, `proto/`, `gen/`, `tasks/` and `web/`. The rest comes with the beta. E2 has one prompt, the sheet's, in three variants — with the key, without it and reading only (R62) — and the task that writes the Gemini adapter settles which package holds it. The beta's prompts follow what E2 decides.
### The dependency rule
Dependencies point inward: `app` → `transport` → the use cases (`check` and its siblings) → `domain`, and `infra` implements the ports the use cases declare. Nothing in `internal/domain/` or in a use case may import `net/http`, the router, the Firebase, OpenFGA or Centrifugo clients, `genai`, pgx or `database/sql`; pgx itself is imported only by `internal/infra/postgres` and `cmd/migrate`.
-A use case reaches the outside world only through the ports it declares (spec 1, 18). A check needs `Authorizer`, `Recognizer`, `Tutor`, `Notifier` and `Store` — the last one, in practice, several narrow storage interfaces, one per consumer, rather than one for everything — and every other use case that checks or changes access declares an `Authorizer` of its own. Their adapters live in `internal/infra/`: Gemini — two types, behind `Recognizer` and `Tutor` — OpenFGA, Centrifugo and Postgres, plus Firebase for sign-in, App Check and deleting an account. The ports are also what keeps a move to the MathTrail platform (k8s + EDA) a change of adapters rather than of the domain (spec 19).
+A use case reaches the outside world only through the ports it declares (spec 1, 18). In E2 a check declares one: the port for the model's analysis of the sheet (`Analyzer` in the examples below), which the Gemini adapter implements. It needs no `Authorizer`, `Notifier` or `Store`: E2 has no access rights to check, no events to publish and no database, and its daily ceiling is a counter in memory (R64). The beta's check needs `Authorizer`, `Notifier` and `Store` — the last one, in practice, several narrow storage interfaces, one per consumer, rather than one for everything — and its model ports follow what E2 decides: one, or `Recognizer` and `Tutor` for two calls (R38, R39). Every other use case that checks or changes access declares an `Authorizer` of its own. The adapters live in `internal/infra/`: Gemini, OpenFGA, Centrifugo and Postgres, plus Firebase for sign-in, App Check and deleting an account. The ports are also what keeps a move to the MathTrail platform (k8s + EDA) a change of adapters rather than of the domain (spec 19).
A domain package reaching for infrastructure is not a style problem — it means the code is in the wrong package. Move the I/O out to a use case, `infra/` or `transport/` and leave the decision-making behind. Comparing with the key does not call Gemini:
```go
// wrong — the comparison now needs Gemini, a network and credentials to be tested
-func Judge(ctx context.Context, img canvas.AnswerImage, task Task) (Verdict, error) {
- reading, err := gemini.Read(ctx, img) // the domain doing I/O
+func Judge(ctx context.Context, img canvas.SheetImage, task Task) (Verdict, error) {
+ analysis, err := gemini.Analyze(ctx, img, task) // the domain doing I/O
...
}
// right — a pure function over what was read; check gets the reading
-// through the Recognizer port and hands it in
+// through its port and hands it in
func Judge(key Key, reading Reading) Verdict
```
@@ -179,22 +186,24 @@ As in `mentor-api`: `NewX(...)` returns the interface, and the implementation st
```go
// internal/check — the port, declared by the side that needs it
-type Recognizer interface {
- Read(ctx context.Context, img canvas.AnswerImage, form answer.Form) (answer.Reading, error)
+type Analyzer interface {
+ Analyze(ctx context.Context, img canvas.SheetImage, task answer.Task) (Analysis, error)
}
// internal/infra/gemini — the adapter
-type reader struct{ client *genai.Client } // unexported
-func NewReader(c *genai.Client, model ModelID) check.Recognizer // the constructor returns the port
+type analyzer struct{ client *genai.Client } // unexported
+func NewAnalyzer(c *genai.Client, model ModelID) check.Analyzer // the constructor returns the port
```
+The names are an illustration: the task that writes the port settles its name and contract.
+
This is deliberately *not* the usual Go advice of "accept interfaces, return structs": we keep the platform's convention so that every service in MathTrail reads the same way and mockery always has an interface to generate from. Do not "fix" it back.
Keep them narrow — every interface in `mentor-api` has one to three methods. Past five, split it: an interface that large usually means the type does several jobs.
-Extend by adding an implementation, not another branch. A second `switch` over kinds of recognizer or kinds of notifier is the signal to reach for an interface instead.
+Extend by adding an implementation, not another branch. A second `switch` over kinds of model provider or kinds of notifier is the signal to reach for an interface instead.
-Implementations of the same interface must be interchangeable, and that is worth proving in code: when a port gets a second implementation — another model provider behind `Recognizer`, say — both run one shared contract test set.
+Implementations of the same interface must be interchangeable, and that is worth proving in code: when a port gets a second implementation — another model provider behind `Analyzer`, say — both run one shared contract test set.
### Simplicity beats purity
@@ -229,18 +238,18 @@ Implementations of the same interface must be interchangeable, and that is worth
- JSON encoder with the Cloud Run keys `severity`, `message` and `time`. (`mentor-api` logs `ts` in ISO8601 — different runtime, different keys.)
- Lowercase messages, snake_case keys.
- A line carries IDs, codes, timings, `prompt_version` and the model's ID — nothing that "Children's data" above keeps out (spec 16). Every model call logs its `prompt_version` and model ID (spec 7.4).
-- The log is held to a table of its events and the fields each one may carry, not to a list of what it must not hold: a field nobody decided on fails the test the day it is added, before any value it carries can leak (spec 16; standalone: R141, R169). The table and its test come with the MVP epic.
+- The log is held to a table of its events and the fields each one may carry, not to a list of what it must not hold: a field nobody decided on fails the test the day it is added, before any value it carries can leak (spec 16; standalone: R141, R169). The table and its test come in E2, with the check's endpoint (R65).
### Errors
- Wrap with short context; use sentinel errors where callers branch, checked with `errors.Is`/`errors.As`.
- Log an error once, at the boundary.
- An HTTP error is the flat `apierror` body — `{code, message}` with an optional `details` — carrying a code from spec 8.3; the request's ID travels in the `X-Request-Id` header, not in the body (R11). Never send internal error text to the client.
-- A failure of Gemini or of the check's own code is not an HTTP error: it becomes an `UNCHECKED` verdict or a fallback hint (spec 5.4, 5.5, 9.9). The store is the exception: when Neon or OpenFGA fails, or no slot frees up to decode the images, the answer is `UNAVAILABLE`, and the client's retry with the same `client_check_id` finishes the check (spec 8.3, 8.4, Т-14). A panic in `answer` is recovered at its boundary into `UNCHECKED` with the reason `internal`; one in `tutor`, after the verdict is out, into a fallback hint. A failed publish to Centrifugo is logged, and the HTTP response still carries the events.
+- A failure of Gemini or of the check's own code is not an HTTP error: it becomes an `UNCHECKED` verdict or a fallback hint (spec 5.4, 5.5, 9.9). In the beta, the store is the exception: when Neon or OpenFGA fails, or no slot frees up to decode the image, the answer is `UNAVAILABLE`, and the client's retry with the same `client_check_id` finishes the check (spec 8.3, 8.4, Т-14). E2 has no store, and a retry is a new call (R60). A panic in `answer` is recovered at its boundary into `UNCHECKED` with the reason `internal`; one in `tutor`, after the verdict is out, into a fallback hint. In the beta, a failed publish to Centrifugo is logged, and the HTTP response still carries the events.
### Context and docs
-- `ctx context.Context` is the first parameter of anything that blocks or does I/O; slow calls get their own child timeout — Gemini 4 s for reading the answer and 8 s for the draft analysis, both trimmed to what is left of the request's 25 s (spec 7.1, 8.4).
+- `ctx context.Context` is the first parameter of anything that blocks or does I/O; slow calls get their own child timeout. In E2 the one Gemini call gets a timeout that the task writing the adapter picks below Cloud Run's 30 s request timeout, with room left for the rest of the request. The beta's timeouts — today 4 s for reading the answer and 8 s for the draft analysis, both trimmed to what is left of the request's 25 s (spec 7.1, 8.4) — are set again by what E2 measures.
- A request finishes its work before it answers: with request-based billing, Cloud Run gives almost no CPU to a goroutine left running after the response (R23).
- Every package has a package comment; every exported identifier has a doc comment starting with its name. Comments explain *why*.
- `internal/version` variables are set through `-ldflags -X` in both the Dockerfile and CI builds.
@@ -250,14 +259,14 @@ Implementations of the same interface must be interchangeable, and that is worth
- Standard `testing` with `t.Fatalf`/`t.Errorf` in "got …, want …" form — the house style in `mentor-api`, where testify appears only as `mock.Anything` in generated mocks. That is the sense in which spec 3.2 lists testify "as in the ecosystem".
- Table-driven subtests (`t.Run`) for similar cases, with `t.Parallel()` where a case shares no state; external `_test` packages for public behaviour, internal ones for unexported helpers.
- Test helpers take `*testing.T` and start with `t.Helper()`, so a failure points at the test that broke, not at the helper.
-- Anything that parses what came from outside gets a fuzz test (`FuzzXxx(f *testing.F)`): the model's JSON of both calls, the task JSON, the check request and its PNGs — and the responses and request bodies that carry them. That input is untrusted, a panic there takes a request handler down with it, and fuzzing is how those inputs get found before a child's handwriting finds them.
+- Anything that parses what came from outside gets a fuzz test (`FuzzXxx(f *testing.F)`): the model's JSON, the task JSON, the check request and its PNG, the export of the author's journal — and the responses and request bodies that carry them. That input is untrusted, a panic there takes a request handler down with it, and fuzzing is how those inputs get found before a child's handwriting finds them.
- Computation that holds an invariant gets a property test with `gopter`, the answer check first of all (spec 17): normalizing twice changes nothing, any accepted spelling of a number equals a key of the same value and no other, the form's whitelist takes no base prefix, exponent, underscore or redundant leading zero (`07`, `010/2`), and no input panics. The test names the invariant and the library looks for the input that breaks it. A property replaces neither the worked examples of spec 5.6 nor a fuzz test; it is what catches the case nobody thought of.
- A new check counts as covered only when its test fails with the check switched off. Break it, watch the test go red, put it back — that is how a test that passes by accident is found, and it is cheaper than any tool that does the same thing by rewriting the code.
- `go test -race` always; fixtures under `testdata/`.
- Under the race detector a clock measures the machine rather than the code. So a test that holds code to a clock gives it ten times the time there and leaves every limit that is not time as it is; the factor comes from one helper built with and without the `race` tag, not from a constant in each test. A test about the clock itself — that a call is cut off at its deadline, say — sets its own. A run that outlasts its clock is not retried: that would hide code that really is slow (R207 standalone).
- Recorded responses of Gemini in `testdata/` are synthetic — never a child's sample (spec 17). They are the reference a test is checked against, so no test rewrites them. A snapshot the code itself produces, such as a re-encoded check image, may have `-update`, and then the regenerated diff is part of the review.
- No network in unit tests: Firebase, Gemini and Centrifugo are faked with `httptest`.
-- Integration tests run Postgres and OpenFGA in containers, under a build tag of their own (`just ci-integration`, from the MVP epic's task that brings storage).
+- Integration tests run Postgres and OpenFGA in containers, under a build tag of their own (`just ci-integration`, from the beta's task that brings storage).
- Behind an interface stands the real implementation, whenever that implementation does no I/O, and a small hand-written fake where a test has to force it to behave a particular way. That is what proves a property rather than the fact of a call, and it keeps what a test assumes in one readable place.
- A mock earns its place when the real implementation needs the network, a clock, or is simply slow — and when what is being checked is that a call happened, with these arguments. It is then generated by mockery v3 into a `mocks/` folder next to the interface and committed; CI checks it is up to date. Config lives in `.mockery.yaml`, with expecters on, mirroring `mentor-api`, and its list of packages is empty until an interface of that kind exists.
@@ -306,7 +315,7 @@ Inside the container:
- Everything is installed straight from the Dockerfile, and downloads refuse a protocol downgrade (R35 standalone). What is verified is verified by the source it comes from: the Go checksum database for `go install`, the registry hash for the npm package; the tarballs fetched with `curl` carry no checksum of their own. Of the npm packages only the Claude Code CLI may run its own install step, named explicitly, because that step is what puts its native binary in place.
- Docker (docker-in-docker, Moby engine and buildx) comes from a devcontainer feature pinned in `devcontainer.json` and `devcontainer-lock.json`.
- To change a version: edit the literal everywhere it appears (Dockerfile, `devcontainer.json` for Docker or the Claude Code extension), rebuild the container. The Claude Code CLI and its editor extension are one version in two files, so `just claude-update` raises both at once — with a version, or to the newest published one — and prints what it changed. Terraform is pinned twice as well: `ARG TERRAFORM_VERSION` and `required_version` in `infra/terraform/versions.tf`, which refuses any other. Its providers and `.terraform.lock.hcl` are raised by hand too, `terraform providers lock -platform=linux_amd64 -platform=linux_arm64`, since Dependabot would lock them with a Terraform of its own (R56).
-- **Container marker.** `MATHTRAIL_DEVCONTAINER=1` is set only inside the container. A session that does not see it is on the host and must stop (see "Devcontainer only").
+- **Container marker.** `MATHTRAIL_DEVCONTAINER=1` is set only inside the container. A session that does not see it is on the host and must stop, unless it is a cloud session the author started (see "Devcontainer only").
- **Claude Code.** Its config lives in the `canvas-api-claude` volume, so the login survives rebuilds. Auto-update is off; the version is pinned.
- **`gh`.** What a change looks like on github.com is otherwise invisible from in here, and two parts of it are asked for by name: the alerts of the code scan, which a task closes before the next one starts, and whether the delivery that carried the change ran. `gh auth login` is answered once: the credentials live in the `canvas-api-gh` volume, as gcloud's live in `canvas-api-gcloud`, where Terraform reads them.
- **Ports.** 8080 (the API) and 5173 (Vite's dev server for the SPA) are forwarded to the host.
diff --git a/README.md b/README.md
index 023dd15..a3f203d 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
[](https://sonarcloud.io/summary/new_code?id=MathTrail_canvas-api)
[](https://sonarcloud.io/summary/new_code?id=MathTrail_canvas-api)
-MathTrail Canvas is a math tutor for children in grades 1–6. A child solves a task by hand, with a stylus or a finger, on a squared draft sheet and writes the final answer in an answer box. On **Check**, an AI reads the answer and a program compares it with the task's key. When the answer is wrong, the tutor looks at the draft, frames the place where the reasoning went wrong and asks one short guiding question. It never gives the answer.
+MathTrail Canvas is a math tutor for children in grades 1–6. A child solves a task by hand, with a stylus or a finger, anywhere on a squared sheet and writes the final answer on the same sheet — there is no separate answer box. On **Send Answer**, one AI call reads the whole sheet: it finds the final answer, follows the reasoning and finds the first mistake. A program, not the AI, compares the answer with the task's key. When the answer is wrong, the tutor frames the place where the reasoning went wrong and asks one short guiding question. It never gives the answer.

diff --git a/RUN.md b/RUN.md
index a45e369..06d0f99 100644
--- a/RUN.md
+++ b/RUN.md
@@ -27,7 +27,7 @@
- Это разрешённое отступление от правила 1.
- Находку анализатора сначала чинить кодом. Пометка «ложное» — крайняя мера: с причиной, названной словами, и с тестом, который это доказывает (standalone: R35, R50, R111). Находка, которую правка закрыть не может, остаётся открытой с записанной причиной (R51 standalone). Молча находку не пропускать.
- Эти правки идут в отчёт отдельным абзацем — я коммичу их отдельно от задачи.
-4. Все задачи, кроме T00–T03, выполняются только внутри devcontainer. T00–T03 шли на хосте: контейнер создаёт T03. Если сессия запущена не в нём (`MATHTRAIL_DEVCONTAINER` не равен `1`), остановиться и попросить открыть проект в контейнере.
+4. Все задачи, кроме T00–T03, выполняются только внутри devcontainer. T00–T03 шли на хосте: контейнер создаёт T03. Если сессия запущена не в нём (`MATHTRAIL_DEVCONTAINER` не равен `1`), остановиться и попросить открыть проект в контейнере. «Правка — только документы» — не повод продолжать: остановиться до первой правки (решение автора 2026-10-16, после T61). Единственное исключение — облачная сессия Claude Code, которую автор запустил для задачи: она работает без контейнера, прогоняет те проверки, до которых дотягивается, а отчёт называет каждую, что не шла (решение автора 2026-10-16).
5. Gemini, Google Cloud, Firebase и Apple Developer стоят денег. Живые прогоны, спайки и действия в облаке — только после предупреждения с ожидаемой стоимостью и моего подтверждения.
6. Код SDK, протоколов и API не писать по памяти: сначала свериться с документацией закреплённой версии (Firebase Admin SDK, OpenFGA go-sdk, `google.golang.org/genai`, Centrifugo v6, pgx, goose, buf, Konva, centrifuge-js, Firebase JS SDK, провайдеры Terraform). Ссылка на документацию идёт с датой обращения — в отчёте и в решении или документе, которые на неё опираются. Если документация расходится с закреплённой версией, эталон — сама библиотека (R06 standalone).
7. **Задача с кодом заканчивается зелёными проверками** — теми рецептами, которые уже есть и касаются её правок:
@@ -140,8 +140,8 @@
| Статус | ID | Задача | Кто | План | Зависит от |
|---|---|---|---|---|---|
-| [ ] | T61 | Документы: журнал решений и CLAUDE.md | Claude, проверяю я | — | — |
-| [ ] | T62 | Документы: product, privacy, spec, stack | Claude, проверяю я | — | T61 |
+| [x] | T61 | Документы: журнал решений и CLAUDE.md | Claude, проверяю я | — | — |
+| [x] | T62 | Документы: product, privacy, spec, stack | Claude, проверяю я | — | T61 |
| [ ] | T16 | Каркас сервиса | Claude | — | T61 |
| [ ] | T63 | Terraform: провайдеры 8.7.0 | Claude | — | — |
| [ ] | T64 | Облако E2: секрет ключа, Agent Platform, бюджет | Claude | да | T63 |
@@ -261,6 +261,9 @@
| В3 | Включить CodeQL для workflow (язык `actions`)? | T07 | Да, отдельной задачей. actionlint проверяет синтаксис и контракты вызовов, а CodeQL — путь недоверенных данных в скрипты и права заданий. Его новые находки High будут держать мерж |
| В4 | Удалять после живого прогона весь `.claude/settings.local.json` или только правила прогона? | T59 | Только правила прогона, если в файле есть и другие настройки; весь файл — если его создали ради прогона |
| В5 | Что заменит журнал решений, когда удалят `docs/decisions.md`? На него опираются правила CLAUDE.md — пометка находки анализатора ссылается на решение, журнал читают перед правкой дизайна, — а номера R стоят в RUN.md, spec, product, privacy и `docs/stack.html` | после T11 | Решить до удаления. Пометка находки может называть причину словами — так теперь пишутся комментарии в коде |
+| В6 | Кто правит под E2 `draft/README.md` и spec 18? README макета описывает поле ответа с кнопкой Check, два вызова и пять задач. Spec 18 называет порты `Recognizer` и `Tutor`, конструкторы `NewReader` и `NewTutor` и middleware беты. В список T62 они не входят, а CLAUDE.md, который ссылается на spec 18, уже говорит, что E2 делает иначе | T61 | T62 сделан без них. Одной правкой документов вместе с В7, до T16 — он следующий и читает spec 18: в README макета — E2 в разделе «Where the documents differ»; в spec 18 — плашку E2, как в 3.1, 9, 10 и 12.2 |
+| В7 | Кто правит spec 13, 14 и 17 под E2? 13 говорит, что чтение не видит ключа, — в E2 видит (R62); 14.1 расписывает задержку двух вызовов и ссылается, как и 17, на спайки S1 и S2, которые заменил эксперимент E2. В список T62 (spec 1–8, 15.2, 20) они не входили | T62 | Вместе с В6: в 13 и 14.1 — оговорку «E2», как в разделе 1; S1 и S2 в 14.1 и 17 — на eval E2 (T74–T76) |
+| В8 | Сколько хранятся образцы автора E2 — журнал в IndexedDB и выгрузки в `samples/`? Privacy 4 и 8 пишут «решает автор»: решения о сроке нет ни в журнале решений, ни в задачах | T62 | Как eval-наборы: до конца исследования, не больше 12 месяцев (privacy 4). Почерк — свой, но образцы те же, что идут в eval |
## Таблица 4. Технический долг
@@ -287,6 +290,8 @@
| Д19 | Повтор проверки после обрыва — новый вызов Gemini: нет ни идемпотентности по `client_check_id`, ни аренды (R42) | E2, план 2026-10-15 | Лишняя оплата и два ответа на одну попытку | E15: протокол проверки spec 8.4 |
| Д20 | Правило 14 и телеметрия в E2 не действуют: нет инструмента нагрузки, стражей конкурентности и трасс | E2, план 2026-10-15 | Сроки, потолок и вызовы модели не проверены нагрузкой; задержку видно только в журнале автора и в логах | E15, первой задачей — до первой ручки с базой |
| Д21 | Перо и ладонь на планшете (spec 3.1) не проверены: эксперимент идёт на компьютере | E2, решение автора 2026-10-15 | На iPad и Android ладонь может рисовать, а перо — вести себя иначе, чем мышь | E15, вместе с Д4 |
+| Д22 | Список «Build context» в CLAUDE.md (Environment) должен назвать `samples/`, когда задача журнала добавит его в `.dockerignore` | T61 | CLAUDE.md разойдётся с `.dockerignore`: следующая сверка не узнает, что образцы держатся вне сборки нарочно | T72 |
+| Д23 | Порт `Analyzer` в примерах CLAUDE.md (The dependency rule, Interfaces) — иллюстрация. Имя и контракт порта выбирает задача адаптера Gemini | T61 | Если T35 выберет другое имя, примеры CLAUDE.md разойдутся с кодом | T35 |
## Эпик E2. Минимальный срез: Gemini проверяет решение с листа
@@ -366,6 +371,60 @@
- каждое решение этого раздела есть в журнале;
- CLAUDE.md не противоречит журналу.
- **Не делать:** product, privacy, spec и `docs/stack.html` — это T62.
+- **Сделано (2026-10-16):**
+ - **`docs/decisions.md`** — восемь записей. Каждое решение раздела «Эпик E2» стоит в одной из них:
+ - R60 — E2 как срез и эксперимент, бета — E15, с таблицей «что → в E2 → в E15»: решения 10–12; база, повтор после сбоя, телеметрия и нагрузка;
+ - R61 — лист без поля ответа и Send Answer: решения 1 и 2; геометрия листа — один источник;
+ - R62 — один вызов с ключом, вердикт программы, итог прочитан дважды, результат, контрольные варианты eval: решения 3 и 4; кеш Gemini — ресурсом Terraform `google_vertex_ai_cache_config`, он есть в провайдере 8.3.0 (документация ресурса на теге v8.3.0, прочитана 2026-10-16);
+ - R63 — шесть задач, заменяет R40: решение 5; после шестой — снова первая;
+ - R64 — ключ и дневной потолок, вход и App Check — в E15: решение 6; потолок в памяти, адрес API в SPA, облако одной задачей T64;
+ - R65 — панель, разметка, журнал, выгрузка, eval и лог; исключение из «ответ скрыт до решения» на время E2; образцы в `samples/`: решения 8 и 9;
+ - R66 — векторы штрихов — исследование T77: решение 7;
+ - R67 — Т-1 закрыт: gin.
+
+ Номера задач и архив черновика — в поправке R59.
+ - **Поправки 2026-10-15** на месте: R22, R23, R24, R35, R38, R39, R42, R44, R59 — по списку задачи; сверх него R56 — бюджет $20 на время E2, иначе журнал спорил бы сам с собой. R40 получила пометку «Заменено 2026-10-15 — R63», как записи, заменённые поворотом 2026-10-11. Вступление журнала называет новые записи и поправки.
+ - **`CLAUDE.md`:**
+ - The project — лист без поля ответа, Send Answer, E2 и E15, эксперимент на компьютере; в каждом пункте Technical shape — что E2 делает иначе: ключ вместо App Check и ID token, один вызов, ни базы, ни Centrifugo;
+ - Hard rules — что получает Gemini в E2, ключ во входе, исключение панели автора; ещё — образцы автора в `samples/` и то, что детей в E2 нет;
+ - Layout — `cmd/eval`, промпт листа, middleware E2 — CORS, затем ключ; что строит E2, а что — бета; Т-1 — gin;
+ - The dependency rule — в E2 один порт разбора, без `Authorizer`, `Notifier` и `Store`. Примеры кода — на лист и порт `Analyzer` вместо `AnswerImage` и `Recognizer`;
+ - Context and docs — тайм-аут одного вызова E2 вместо 4 и 8 с;
+ - «MVP epic» → E2 (таблица событий лога) или бета (пакеты сценариев, `ci-integration`);
+ - сверх списка задачи, чтобы CLAUDE.md не спорил с журналом: Errors — хранилище и `UNAVAILABLE` только в бете, повтор в E2 — новый вызов; Tests — fuzz на JSON одного вызова и на выгрузке журнала.
+ - **`README.md:19`, `SECURITY.md:39`** — без поля ответа, Send Answer, один вызов. В SECURITY.md поправлены и соседние строки: 38 — одна картинка вместо двух; 41 — исключение панели автора на время эксперимента. Иначе публичная политика обещала бы, что ни одна ручка не отдаёт ответ.
+ - **Проверено:**
+ - `grep -n -i 'answer box' CLAUDE.md README.md SECURITY.md` находит три места, и в каждом сказано, что поля нет;
+ - каждое решение раздела «Эпик E2» сверено с записями журнала (список выше);
+ - CLAUDE.md сверен с журналом.
+ - **Проверки:** правка только документов, кода нет. Из рецептов правила 7 к ней относится только `ci-secrets`: его тело с gitleaks v8.30.1 прошло по всей истории (18 коммитов) и по незакоммиченному — утечек нет. Выключенных проверок и fuzz нет.
+ - **Ревью `/code-review high`, два круга.**
+ - Первый — 10 находок. Исправлено 8:
+ - R66 противоречил себе: «модели штрихи не уходят», а T77 шлёт их модели в eval;
+ - R56 держал бюджет $1 — добавлена поправка;
+ - R62 называл ресурс кеша без источника — сверено с документацией провайдера на теге v8.3.0;
+ - двусмысленный список «что строит E2» в CLAUDE.md;
+ - CLAUDE.md называл `samples/` уже игнорируемым — теперь сказано, кто добавит строки;
+ - поправки R39 и R42 не снимали своих «Отвергнуто» — черновик в Gemini на каждую проверку и PNG для контроля качества;
+ - правило о входе Gemini опиралось только на privacy 5.1, которую правит T62, — теперь и на R62;
+ - эксперимент на компьютере, а не на планшете, — в The project.
+
+ Отброшена 1: «один вызов» в README требует текст задачи, а «It never gives the answer» говорит о ребёнке, а не о панели автора. Ещё 1 — правка вне devcontainer, она выше, в «Среде».
+ - Второй — 9 находок. Исправлено 6:
+ - R64 называл бюджет пределом трат, а он только шлёт письма;
+ - правило CLAUDE.md о входе Gemini не называло eval;
+ - фраза о Centrifugo в Errors — теперь только о бете;
+ - SECURITY.md:41 спорил сам с собой в одной строке;
+ - отложенное из «Открыто» — в таблицу 4 (Д22, Д23);
+ - пометка R40 «до разбиения E2» → «до поправок E2»: коммит 24b94f2 сам разбил E2, но product и spec в нём ещё прежние.
+
+ Отброшены 2: «one call» в SECURITY.md:39 требует текст задачи; локальный ключ в Config не нужен — T65 берёт ключ из `.env`, переключателя нет, а остальное решит её план. Ещё 1 — снова правка вне devcontainer.
+ - Отдельной находкой второго круга был непрогнанный `ci-secrets` — его итог выше. Третьего круга по правилу нет.
+ - **Среда — отступление от правила 4** и от «Devcontainer only» в CLAUDE.md. Автор запустил задачу в облачной сессии Claude Code, где `MATHTRAIL_DEVCONTAINER` не задан. Правило велит остановиться и попросить открыть контейнер; я продолжил, потому что открыть контейнер в облачной сессии нельзя, а правка — только Markdown и не зовёт ни одного инструмента контейнера. Автор 2026-10-16: впредь — только в devcontainer, без исключений; правило 4 и CLAUDE.md дополнены.
+- **Открыто:**
+ - `draft/README.md` и spec 18 не правит ни одна задача E2 — вопрос В6 в таблице 3;
+ - `samples/` в списке «Build context» CLAUDE.md — долг Д22 (T72); имя порта `Analyzer` в примерах CLAUDE.md — иллюстрация, долг Д23 (T35);
+ - product, privacy, spec и `docs/stack.html` ещё говорят о поле ответа и двух вызовах — это T62.
- **Разделы:** раздел эпика E2; R22–R24, R35, R37–R44, R59.
#### T62. Документы: product, privacy, spec, stack
@@ -396,6 +455,60 @@
- product 7 и 12 не противоречат таблице 1.
- **Не делать:** переписывать spec 4–8 целиком — это задачи, названные в плашках.
- **Разделы:** product 1, 5–7, 12, 14, 15; privacy 1, 4, 5, 11, 12; spec 1–8, 15.2, 20.
+- **Сделано (2026-10-16):**
+ - **product:**
+ - шапка — MVP это бета E15, до неё E2; что E2 делает иначе, помечено «E2» на месте;
+ - 1–4 — лист без поля ответа, Send Answer, компьютер в E2, шесть задач (R63), в E2 пользуется только автор;
+ - 5.1–5.7 — экран без поля ответа и панель автора E2; цикл E2 «Send Answer → один вызов → вердикт программы → Next task» с новой схемой; фразы, в том числе «Circle your answer.» для нескольких ответов или ни одного; лестница и «решить заново» — в бете;
+ - 6 — сценарии помечены как бета; С2–С4 без поля ответа;
+ - 7 — объём беты и отдельно объём E2, с тем, что из беты в E2 не входит;
+ - сверх списка, где говорилось о поле ответа, Check или спайках S1 и S2: 8, 9 (расчёт помечен как двухвызовный, до E2; бюджет E2 $20), 10 (цели — для беты), 11, 13 (риск «ИИ принимает за ответ не то число»);
+ - 12 — критерии приёмки относятся к бете; у E2 приёмки нет, его метрики меряют журнал и eval; новая метрика «ответ найден» (R61);
+ - 14 — E15 первым; 15 — О-7 и О-10 по R63, О-12 и О-16 — что в E2.
+ - **privacy:** шапка — документ о бете, в E2 детей нет; 1, 3, 6, 7, 8, 10, 11 — одна картинка листа вместо двух; 4 — строки листа и картинки, строка образцов автора E2 и абзац «В E2»; 5.1 — один вызов E2 (лист, условие, класс, ключ, решение, ничего о человеке), а схема двух вызовов — для беты, пока её не решат выводы E2; 5.2 — кеш выключает ресурс Terraform; 5.3 — сверка `value` с `written`; 8 — срок образцов автора; 12 — П-8 пересматривается, если T77 выберет штрихи; П-10 — при разбиении E15.
+ - **spec:**
+ - шапка — что E2 и где плашки;
+ - 1 и 2 — лист, один вызов E2 рядом со схемой беты, один порт разбора, что из архитектуры работает в E2;
+ - 3.2 — gin (R67); 3.3 — модели E2;
+ - плашки «раздел переписывает Txx» — 4 (T67), 5 (T66), 6 (T34), 7 (T35), 8 (8.2 — T18, 8.6 — T19, остальное — T70: их тела правят свои части), 10.3–10.6 (T35);
+ - плашки беты — 3.1, 9, 12.2 и сверх списка 10 (кроме 10.3–10.6): без неё `grep` критерия находил поле ответа в примерах;
+ - 15.2 — кеш выключает `google_vertex_ai_cache_config`, сверяет T64, иначе рецепт; API Secret Manager и Agent Platform — T64; 19 — одна картинка листа;
+ - 20 — Т-1 решён (R67); Т-5 — по eval и выводам E2; Т-6 — при разбиении E15: лист E2 рисуется без клетки, и E2 её не меряет; Т-16 — без поля ответа, закрывает T67; S1 и S2 заменены экспериментом E2, их цели — цели беты, а сотня листов E2 — разведка (T74).
+ - **`docs/stack.html`** — по строкам: каждая строка с «E2» сверена — что строит E2, осталось E2 со своей задачей; что строит бета, стало E15. Т-1 ушёл из «Что ещё предстоит выбрать», gin — «по спеке» (R67); Т-5, Т-16, S1 и S2 — на свои задачи E2, Т-6 — на разбиение E15; новые строки: ключ доступа и один вызов Gemini E2; подвал — статусы на 2026-10-16.
+ - **Отступление от буквы задачи:** плашка беты сказана не «E2 — без поля ответа; раздел описывает бету», а «поля ответа нет ни в E2, ни в бете (R61) … раздел описывает бету по плану до E2, с полем ответа, и переписывается при её разбиении». Бета тоже без поля ответа, и прежний текст плашки читался бы так, будто поле в бете есть.
+ - **Проверено:**
+ - `grep -n 'поле ответа\|поля ответа' docs/product.md docs/privacy.md docs/spec.md` — в product и privacy только места, где сказано, что поля нет; в spec ещё — под плашками E2 (3.1, 4–10, 12.2);
+ - product 7 и 12 сверены с таблицей 1: шесть задач, Send Answer, один вызов, ключ и потолок, журнал и eval, Next; приёмка — бете;
+ - скрипт `docs/stack.html` разбирается (`node --check`), в таблице 112 строк.
+ - **Проверки:** правка только документов. Из рецептов правила 7 к ней относится `just ci-secrets` — зелёный: 22 коммита и незакоммиченное, утечек нет. Выключенных проверок и fuzz нет.
+ - **Ревью `/code-review high`, два круга.**
+ - Первый — 10 находок, исправлены все:
+ - product 5.6 позволял в E2 новый Send Answer после вердикта, а 5.3 и T71 ведут только к Next task;
+ - privacy 11 — «Лист … Они стираются»;
+ - Т-16 в `docs/stack.html` называл книжный лист 1000 × 1400, а T67 планирует один альбомный экран;
+ - product не называл исключение панели автора из «ответ не показывается» (R65) — добавлено в 5.1;
+ - абзац «В E2» privacy 4 спорил со строкой образцов автора в той же таблице;
+ - «токены» в составе лога читались как токены доступа — теперь «число токенов» (privacy 4, `docs/stack.html`);
+ - «разбор черновика» рядом с «разбором решения» в одной таблице privacy 4, в product 12 и 13 — одно имя;
+ - spec 2: строка E2 стояла внутри пункта «App Check обязателен» — вынесена в свой пункт;
+ - В6 предлагал «добавить в T62», а T62 сдан — теперь «вместе с В7, до T16»;
+ - дата О-12 — 2026-10-15, как у О-7 и О-10.
+ - Второй — 10 находок, исправлены все:
+ - плашка 10 и шапка spec не знали, что T35 переписывает примеры 10.3–10.6;
+ - В6 называл читателями spec 18 T35 и T67, а читает его T16;
+ - Т-6 «по eval E2» — молча закрытая дыра: лист E2 рисуется без клетки, и eval её не меряет. Теперь — при разбиении E15;
+ - S1 и S2 держали выводы E2 целями product 12, а T74 называет сотню листов разведкой;
+ - срок образцов автора — вопрос В8 таблицы 3, а не только «Открыто»;
+ - `secrets.tf` в 15.2 — секрет ключа E2;
+ - privacy 5.2 обещал ресурс Terraform без оговорки, что T64 может вернуть рецепт;
+ - Pointer Events в `docs/stack.html` — ладонь и палец в бете;
+ - privacy 11 обещал задачу на каждую проверку, а в двух вызовах беты она уходит после неверного ответа;
+ - плашка 8 называла только T70, а 8.2 правит T18 и 8.6 — T19.
+ - Отброшенных нет. Третьего круга по правилу нет; исправления второго прошли `node --check` и `just ci-secrets`.
+- **Открыто:**
+ - `draft/README.md` и spec 18 — по-прежнему вопрос В6 таблицы 3: в список T62 они не вошли;
+ - spec 13 говорит, что чтение не видит ключа, spec 14.1 расписывает два вызова, а 14.1 и 17 ссылаются на спайки S1 и S2, которые заменил E2. Они вне списка T62 (spec 1–8, 15.2, 20) — вопрос В7 таблицы 3;
+ - срок хранения образцов автора privacy 4 и 8 называет «решает автор»: это его собственные образцы, и решения о сроке нет ни в журнале, ни в задаче — вопрос В8 таблицы 3.
#### T16. Каркас сервиса
diff --git a/SECURITY.md b/SECURITY.md
index 10940ca..f22af17 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -35,10 +35,10 @@ Research in good faith is welcome, and nobody will be pursued for it. Two limits
The service is still being built, in the order [`RUN.md`](RUN.md) sets; this is what it is designed to hold, and every change is held to it. The full register, in Russian, is section 4 of [`docs/privacy.md`](docs/privacy.md).
- **About a child, only a pseudonym, an avatar from a set and the grade.** No name, photo, birth date, school or contacts is asked for or kept. The parent owns the account, and no child's data exists before the parent's consent is verified.
-- **The handwriting stays on the device.** The child's strokes live in the browser. On Check the app sends two pictures drawn from them, and the server keeps the pictures only in the memory of that request — never in the database, a log or a trace.
-- **The AI receives what is listed, and nothing more.** Gemini gets the answer box's picture to read it and, after a wrong answer, the draft's picture with the task, so that it can frame the mistake. No identifier, no pseudonym and no parent's data goes with them, and Gemini's caching is switched off.
+- **The handwriting stays on the device.** The child's strokes live in the browser. On Send Answer the app sends one picture of the whole sheet, drawn from them, and the server keeps it only in the memory of that request — never in the database, a log or a trace.
+- **The AI receives what is listed, and nothing more.** In one call, Gemini gets the picture of the sheet with the task, its key and its solution, so that it can find and read the final answer, frame the mistake and word one question; whether the answer is right is a program's comparison with the key, not the AI's. No identifier, no pseudonym and no parent's data goes with them, and Gemini's caching is switched off.
- **No personal data in logs, metrics or traces.** They carry IDs, codes and timings: an ID is a UUID or the parent's Firebase UID, never an email, a name, a pseudonym, a picture, a read answer or the text of a hint.
-- **The task's answer stays hidden** until the task is solved: no endpoint returns it before then, and every hint the AI writes is checked for leaking it before the child sees it.
+- **The task's answer stays hidden** until the task is solved, and every hint the AI writes is checked for leaking it before the child sees it. No endpoint returns the answer before then, with one temporary exception: while the service is an experiment that only its author uses, behind a key, a check's response carries the AI's whole analysis — which saw the key and may name it — to the author's panel.
- **No secrets in this repository.** Keys live in Google's Secret Manager and reach the service through its environment. The whole history is scanned for secrets on every pull request, and a key that was ever committed would be treated as leaked and rotated.
## Supported versions
diff --git a/docs/decisions.md b/docs/decisions.md
index ce57bf2..95f8687 100644
--- a/docs/decisions.md
+++ b/docs/decisions.md
@@ -7,6 +7,8 @@
> Первые записи собраны 2026-09-27 при разборе исходного черновика автора (`tmp.spec.md`; в репозиторий он не входит, все его утверждения, с которыми мы не согласны, пересказаны здесь). Записи R26–R31 и правки R14, R16, R20, R21, R23, R24 внесены по итогам ревью документов 2026-09-27. Записи R32–R34 внесены при составлении плана реализации ([RUN.md](../RUN.md)) 2026-09-27. Записи R35 и R36 внесены 2026-09-27 по решениям автора. Продуктовые вопросы (О-…) — в [product.md](product.md), раздел 15; технические (Т-…) — в [spec.md](spec.md), раздел 20; вопросы приватности (П-…) — в [privacy.md](privacy.md), раздел 12.
>
> Записи R37–R43 внесены 2026-10-11 при повороте к LLM-first. Записи, которые они заменили, остались на месте с пометкой «Заменено»: их текст не правится, а их «Отвергнуто» больше не действует. Записи, которые поворот только задел, поправлены на месте, и в конце каждой стоит «Поправка 2026-10-11». Запись R44 о плане эпиками внесена тогда же, по решению автора. Запись R45 о выкладке после релиза — тоже, при сверке с mathtrail-standalone. Запись R46 о намеренных отличиях devcontainer от образца внесена 2026-10-12 при сверке с ним (T60). Записи R47 о тулчейне SPA и R48 о лицензиях npm внесены тогда же, в T05. Запись R49 о CI и его отличиях от образца внесена 2026-10-13 в T06. Записи R50 о том, что держит мерж, и R51 о находках, которые остаются открытыми, внесены тогда же, в T07.
+>
+> Записи R60–R67 внесены 2026-10-16 (T61) по решениям автора 2026-10-15, когда E2 стал минимальным срезом с замером Gemini, а закрытая бета — эпиком E15. Запись R40 они заменили, и она осталась на месте с пометкой «Заменено». Записи R22–R24, R35, R38, R39, R42, R44, R56 и R59 поправлены на месте, и в конце каждой стоит «Поправка 2026-10-15».
## Рамки и продукт
@@ -56,6 +58,8 @@ MVP — математика 1–6 класса с интерфейсом на
Заменяет R03, R17, R18, R19. — product 1, 5, 7, 9; spec 1, 5–7; privacy 3, 5.
+> **Заменено 2026-10-15 — R63.** В E2 шесть задач, по одной на класс 1–6, и пишет их Claude. Текст ниже не правится; его ссылки — на редакцию документов до поправок E2 (коммит 24b94f2 в main).
+
**R40. Пять статичных задач из макета в `tasks/` — временно, до каталога; связи с MathTrail нет.** Это пять из шести задач `draft/MathTrail Minimal.html`:
- лестница (ответ 7);
- задуманное число (9);
@@ -88,6 +92,61 @@ MVP — математика 1–6 класса с интерфейсом на
Заменяет R04. — product 4, 7, 14, 15; spec 8.6.
+**R60. E2 — минимальный срез с замером Gemini; закрытая бета — эпик E15.** Решение автора 2026-10-15. Главное допущение продукта — модель в GCP понимает свободное решение ребёнка и верно проверяет его ответ — до сих пор не проверено: спайки S1 и S2 не проводились. Поэтому раньше беты с родителями, согласием и правами идёт E2 — кратчайший путь к живой проверке в prod и к цифрам:
+- в prod шесть задач, по одной на класс 1–6 (R63). Ребёнок решает где угодно на листе и отправляет его кнопкой Send Answer (R61); один вызов Gemini разбирает лист, а программа сверяет итог с ключом (R62);
+- каждая проверка измерена и размечена автором, а журнал прогоняется на других моделях и настройках (R65);
+- выводы E2 (T76, T77) решают, какой будет проверка в бете.
+
+Прежний E2 — закрытая бета — теперь эпик E15, первый в таблице 2 RUN.md. «MVP» в документах — это она.
+
+Что решено для E2 и что достаётся E15:
+
+| Что | В E2 | В E15 |
+|---|---|---|
+| Лист | без поля ответа, кнопка Send Answer (R61) | так же |
+| Вызовы Gemini | один, с ключом во входе (R62) | один или два — по выводам E2 (T76) |
+| Что уходит на проверку | PNG листа (R42) | PNG или штрихи — по исследованию T77 (R66) |
+| Задачи | шесть, по одной на класс 1–6 (R63) | решит разбиение E15; каталог — E4 |
+| Кто пользуется | только автор, изображая ребёнка; детей нет | семьи: родитель и его дети (R06, R07) |
+| Доступ | ключ автора (R64) | вход родителя через Firebase и App Check (R14; spec 8.1, 12.1) |
+| Согласие, инвайты, PIN, профили детей, OpenFGA | нет | R07, R26, R28, R29 |
+| Лимиты | дневной потолок экземпляра (R64) | лимиты R35 |
+| Подсказки | вопрос модели или запасной текст, без лестницы (R24, R39) | лестница R39 |
+| Realtime | нет: всё приходит HTTP-ответом (R23) | Centrifugo (R23, R41) |
+| Хранение | базы нет; журнал — на устройстве автора (R65) | Neon; лист — в IndexedDB между перезагрузками (R42) |
+| Повтор после сбоя | новый вызов модели | аренда и `client_check_id` (R42; spec 8.4) |
+| Разбор модели | целиком — панели автора (R65) | детскому экрану не уходит |
+| Удаление и очистка | удалять нечего: сервер ничего не хранит | R27; privacy 7 |
+| Телеметрия, нагрузка, e2e | нет; правило 14 RUN.md не действует | телеметрия и нагрузка — первой задачей, до первой ручки с базой |
+| Устройство | компьютер: мышь или перо графического планшета | планшеты: перо, ладонь, туннель cloudflared |
+| Бюджет | $20 в месяц; был $1 | решит разбиение E15 |
+
+— *Почему:* решение автора. Прежде чем строить бету, автор хочет увидеть, как модель справляется со свободным решением, и узнать скорость и цену проверки. Без базы, входа, прав и realtime живая проверка доходит до prod быстрее всего. Продукт будет меняться по ходу, и то, что решат выводы E2, бета строит один раз.
+- Centrifugo и OpenFGA из стека не уходят: R41 в силе, они приходят в E15. В E2 их нет не потому, что «проверка — один запрос и один ответ», — этот довод R41 отверг, — а потому, что в E2 нет беты: ни семей, ни прав, ни экранов, которым нужны события.
+- Эксперимент идёт на компьютере, мышью или пером графического планшета. Планшеты — в E15, вместе с туннелем.
+
+— *Цена* — долги Д16–Д21 таблицы 4 RUN.md: ключ вместо входа, разбор с ключом в ответе, потолок экземпляра вместо лимитов, повтор без идемпотентности, ни телеметрии, ни нагрузки, перо и ладонь на планшетах не проверены.
+
+— *Следствие:*
+- спайки S1 и S2 заменяет эксперимент E2: живой прогон (T75), eval на его журнале (T74) и выводы (T76). S3 и S4 остаются за бетой;
+- черновик T13–T58 — в архиве `docs/plan/E2-draft.md`, сырьё для разбиения E15 (R59).
+
+— *Отвергнуто:* строить бету по прежнему плану и мерить модель уже в ней — главное допущение проверялось бы последним, когда почти вся бета построена.
+
+— RUN.md (раздел «Эпик E2», таблицы 2 и 4); product 7, 12, 14; spec 20; R05, R41, R44, R59.
+
+**R63. Шесть задач, по одной на класс 1–6, пишет Claude; заменяет R40.** Решение автора 2026-10-15.
+- Задачи разнообразны: разные виды ответа — целое с единицей, дробь, вариант из списка — и разные способы решения — например, счёт по рисунку, вычитание столбиком, логика, дроби. Список и тексты Claude отдаёт автору на вычитку до JSON (T19).
+- Формат — spec 8.6 без `simpler_case`: лестницы в E2 нет (R39).
+- После шестой задачи — снова первая. Автор может и сам выбрать задачу 1–6, чтобы решать одну в разных вариантах.
+- Из R40 остаётся: папка `tasks/` — временная, до каталога; `go:embed` и проверка при старте; у задачи — условие, тема, ключ и принятые формы, решение, ловушки с текстами автора, подсказка формы; связи с MathTrail нет.
+
+— *Почему:* решение автора. E2 меряет, как модель понимает разные записи решения (R60), а записи у классов разные: у первого — счёт по рисунку, у старших — дроби. Пять задач макета брались под лестницу и разбор черновика (R40) и такого разнообразия не дают.
+
+— *Отвергнуто:* пять задач макета (R40) — по причине выше. Отвергнутое в R40 — задачи из MathTrail по MCP, фото листа ДЗ, каталог и генерация задач на лету — остаётся отвергнутым.
+
+Заменяет R40. — product 5, 7, 15 (О-7, О-10); spec 8.6; RUN.md (T19).
+
## Архитектура и инфраструктура
**R08. Стек — как в черновике.** Состав:
@@ -481,7 +540,7 @@ Centrifugo сохраняет роль из R23. Событие `verdict` ухо
- кеш провайдеров в задании «Terraform»: задание и так берёт у HashiCorp сам Terraform в каждом прогоне, провайдеры добавляют около 15 с, а хеши lock проверяют их и без кеша;
- общий шаг для чтения версии из `ARG`: три одинаковые строки в action `just`, `node` и `terraform` проще, чем абстракция ради них.
-— `infra/`, `justfile` (`tf-fmt`, `tf-check`, `bootstrap`, `ci-tf-plan`, `ci-tf-apply`, `ci-tf-outputs`, `_tf-redact`), `.github/workflows/ci.yml` («Terraform»), `.github/actions/terraform/`, `.github/dependabot.yml`, `docs/self-hosting.md`; R31–R34, R45, R50, R54, R55, R57; standalone: R26, R27, R29, R32, R33, R266. — *Поправка 2026-10-14 (T11):* аккаунт выкладки SPA, привязки WIF по subject окружений вместо всего репозитория, условный `projectIamAdmin` у аккаунта Terraform; четыре новых теста (R58).
+— `infra/`, `justfile` (`tf-fmt`, `tf-check`, `bootstrap`, `ci-tf-plan`, `ci-tf-apply`, `ci-tf-outputs`, `_tf-redact`), `.github/workflows/ci.yml` («Terraform»), `.github/actions/terraform/`, `.github/dependabot.yml`, `docs/self-hosting.md`; R31–R34, R45, R50, R54, R55, R57; standalone: R26, R27, R29, R32, R33, R266. — *Поправка 2026-10-14 (T11):* аккаунт выкладки SPA, привязки WIF по subject окружений вместо всего репозитория, условный `projectIamAdmin` у аккаунта Terraform; четыре новых теста (R58). — *Поправка 2026-10-15:* на время эксперимента E2 бюджет — $20 в месяц, пороги прежние (R60). Сумму в `prod.auto.tfvars` меняет T64.
**R57. Журналы и трассы проекта — в `us-east4`.** Решение автора 2026-10-14 в планировании T10, по открытому пункту T09:
- **журналы** — в бакете `default-us-east4`, 30 дней (privacy 4, 8). Бакеты `_Default` и `_Required` Cloud Logging создаёт в `global`, и место бакета не меняется (документация Cloud Logging, прочитана 2026-10-14). Поэтому sink `_Default` перенаправлен в новый бакет с прежним фильтром, слово в слово. Бакет `_Default` остаётся пустым;
@@ -575,6 +634,16 @@ Centrifugo сохраняет роль из R23. Событие `verdict` ухо
— `.github/workflows/deploy.yml`, `.github/workflows/release.yml` (задание `deploy`), `.github/actions/{deployment-target,gcloud,firebase}/`, `justfile` (`ci-release-target`, `ci-release-current`, `ci-release-fetch`, `ci-image-push`, `ci-deploy`, `ci-smoke`, `ci-hosting-deploy`, `ci-hosting-check`, `_image-health`, `ci-workflows`), `.github/workflows/ci.yml` («Workflows»), `.github/actionlint.yaml`, `Dockerfile` (стадия `release`), `firebase.json`, `infra/terraform/iam.tf`, `infra/bootstrap.sh`, `docs/self-hosting.md`; spec 15.5; R31, R33, R45, R52, R55, R56; standalone: R29, R30, R32, R33, R154.
+**R67. HTTP-роутер — gin, как в каркасе standalone; закрывает Т-1.** Решено при планировании E2 (план утверждён автором 2026-10-15).
+- `internal/transport/http` переносится из образца на gin вместе с его middleware (T16). CORS и ключ встают за ними (R64).
+- Контракт — protobuf (R12). JSON по правилам protojson — только локально, при `CANVAS_ENV=local`, и никогда при `K_SERVICE` (spec 8.1).
+
+— *Почему:* каркас образца переиспользуется без переделки (R11; рекомендация Т-1 в spec 20). На gin и mentor-api — эталонный Go-сервис платформы, так что сервисы MathTrail читаются одинаково.
+
+— *Отвергнуто:* chi из исходного черновика — HTTP-слой каркаса пришлось бы переписать.
+
+— spec 3.2, 8.1, 20 (Т-1); RUN.md (T16, T18); R11, R12.
+
## Идентификация и доступ
**R14. Для Firebase используем Admin SDK (`firebase.google.com/go/v4`).** Через него проверяем ID token и App Check и удаляем пользователей. `golang-jwt/jwt/v5` нужен только для подписи токенов Centrifugo. `keyfunc` не используется.
@@ -605,6 +674,25 @@ Centrifugo сохраняет роль из R23. Событие `verdict` ухо
— *Отвергнуто:* настройка провайдеров в консоли Firebase вне Terraform. — spec 12.1, 13, 15.2. — *Поправка 2026-10-11:* ключа Mathpix больше нет (R37), поэтому секретов семь, а не восемь.
+**R64. Доступ в E2 — по ключу, плюс дневной потолок вызовов Gemini; вход Firebase и App Check — в E15.** Решение автора 2026-10-15; потолок в памяти и порядок задач — план E2, утверждён тогда же.
+- **Ключ.** Случайный ключ генерирует `ci-tf-apply` в Secret Manager, если версии ещё нет, и нигде не печатает — как секреты R34 (правило 13 RUN.md). Автор вводит его в SPA один раз.
+- **Проверка ключа.** Всё, что за `/v1`, отвечает только запросу с ключом; `/health` и CORS-preflight идут без него. Middleware в E2 — CORS, затем ключ. В E15 ключ уступает место App Check и ID token (spec 8.1).
+- **Дневной потолок** — счётчик вызовов Gemini в памяти экземпляра, при `max-instances: 1`. Резерв берётся до вызова и возвращается, если вызова не было; сделанный вызов засчитывается и при сбое — как в R35. Исчерпан — `UNCHECKED` · `limit`.
+- **Адрес API в SPA** — константа в `web/src`: файлы `.env.*` лежат в `.gitignore`. Выкладка сверяет константу с `service_url`.
+- **Облачную часть ключа и Gemini приносит одна задача (T64):** автор повторяет bootstrap один раз. Переменную ключа сервис получает следующей задачей (T65) — к её выкладке доступ к секрету уже разошёлся по IAM (Д14).
+
+— *Почему:* решение автора. В E2 пользуется только автор (R60), а каждый мерж уходит в prod (R32): платная ручка не должна оказаться открытой ни на одну выкладку. Ключ закрывает её самым коротким путём, а вход через Google и Apple, App Check и инвайты — работа беты (R14, R29, R34). Потолок ограничивает вызовы только на жизнь экземпляра (Д18), а бюджет $20 (R60) лишь шлёт письма на порогах: утёкший ключ по-настоящему закрывает только его смена (Д16).
+
+— *Цена* — долги таблицы 4 RUN.md:
+- Д16: ключ один и живёт в браузере автора. Утёк — платные вызовы доступны любому, пока ключ не сменят;
+- Д18: экземпляр засыпает при простое, и счётчик обнуляется — это потолок на жизнь экземпляра, а не на сутки. При `max-instances` больше 1 он умножается.
+
+— *Отвергнуто:*
+- вход Firebase и App Check в E2 — по причине выше;
+- лимиты R35 в E2: им нужна база, а базы в E2 нет (R60).
+
+Поправляет R35 на время E2. — spec 8.1, 13, 14.5, 15.4; RUN.md (T64, T65, T68, T69); R14, R32, R34, R55.
+
## Холст и проверка ответа
**R15. Координаты хранятся в логической системе страницы.** Страница имеет фиксированную ширину, ось y направлена вниз, масштаб одинаков по обеим осям. Нормализация 0..1000 появляется только на границе с Gemini. — *Почему:* нормализация по ширине и высоте экрана, как в черновике, искажает картинку при других пропорциях экрана. Логические координаты не зависят от устройства. — spec 4. — *Поправка 2026-10-11:* штрихи теперь хранятся на устройстве (R42), но тоже в логических единицах. В этих единицах рисуются картинки проверки и приходит рамка ошибки. На границе с Gemini 0..1000 — это `box_2d` разбора черновика; его переводит сервер (R43). — spec 4.1, 4.4.
@@ -672,7 +760,7 @@ Centrifugo сохраняет роль из R23. Событие `verdict` ухо
— *Отвергнуто:* ввод ответа с клавиатуры (product 2); проверка по паузе; ключ или условие во входе вызова 1.
-Вместе с R43 заменяет R20. — product 5, 6; spec 5, 8.4, 10.3.
+Вместе с R43 заменяет R20. — product 5, 6; spec 5, 8.4, 10.3. — *Поправка 2026-10-15:* поля Answer нет: итоговый ответ ребёнок пишет на листе, где решает, и отправляет лист кнопкой Send Answer (R61). В E2 итог находит и читает тот же единственный вызов, что разбирает лист, и он видит условие и ключ (R62). Это отступление от пункта «ключ или условие во входе вызова 1» из «Отвергнуто» выше — решение автора на время эксперимента, и насколько ключ сдвигает чтение, меряет вариант eval без ключа. Вместо «вызов 1 ключа не видит» итог читается дважды, и программа сверяет `value` с `written` (R62). Остальное в силе: вердикт выносит программа, вердиктов четыре, любое сомнение — `UNREADABLE`, ребёнок видит, что прочитано.
**R42. Черновик живёт на устройстве; на проверку сервер получает две картинки и не хранит их.**
- **Хранение.** Штрихи черновика и поля ответа SPA держит в IndexedDB. Стирает их, когда:
@@ -712,7 +800,7 @@ Centrifugo сохраняет роль из R23. Событие `verdict` ухо
- хранить PNG для контроля качества;
- ластик, который режет штрихи, как пиксельный ластик макета: рамка ошибки привязывается к целым штрихам (R43).
-Заменяет R16. — product 5.2, 10; spec 4, 8.4, 11, 13; privacy 4, 7, 8.
+Заменяет R16. — product 5.2, 10; spec 4, 8.4, 11, 13; privacy 4, 7, 8. — *Поправка 2026-10-15:* поля ответа нет (R61), поэтому картинка проверки одна — PNG всего листа. В E2 она уходит в Gemini на каждую проверку, если лист не пуст и не исчерпан дневной потолок (R62, R64). Ни хранения листа между перезагрузками, ни аренды в E2 нет: базы нет, и повтор после сбоя — новый вызов (R60, долг Д19). Слать ли вместо PNG штрихи, решает исследование T77: автор открыл заново пункт «Отвергнуто» о векторах (R66). Пункт «хранить PNG для контроля качества» в силе: сервер картинок не хранит и в E2. PNG листов хранит журнал на устройстве автора — это образцы автора для eval, а не данные ребёнка (R65).
**R43. Рамка ошибки: сервер переводит `box_2d` модели в координаты листа, клиент привязывает её к своим штрихам.**
- Разбор черновика возвращает `box_2d` в долях 0..1000 картинки черновика. Сервер переводит его в логические единицы листа и пропускает только годный.
@@ -726,16 +814,79 @@ Centrifugo сохраняет роль из R23. Событие `verdict` ухо
Вместе с R38 заменяет R20, поправляет R15. — product 5.5; spec 4.4, 10.8.
+**R61. Поля ответа нет: ребёнок решает и пишет итоговый ответ где угодно на листе, лист отправляет кнопка Send Answer, а итог находит модель.** Решение автора 2026-10-15.
+- Ребёнок рисует, считает и оставляет пометки на одном листе в клетку и там же пишет итоговый ответ. Отдельного поля Answer нет — ни в E2, ни в бете.
+- Итог находит модель. Промпт подсказывает, где его искать: обычно он написан последним, часто с «Answer:» или «=», подчёркнут или обведён. Если итогов несколько или итога нет, это не вердикт, а просьба «Circle your answer» (R62).
+- Кнопка Check становится Send Answer, как в макете. Лист уходит на сервер только по ней; проверки по паузе нет, как и в R38.
+- На проверку уходит одна картинка — PNG всего листа (R42).
+- Геометрия листа — один источник: константы Go, а клиент получает их вместе со списком задач (`ListTasksResponse`). Числа закрепляет T67 и закрывает этим Т-16.
+
+— *Почему:* решение автора. E2 проверяет главное допущение продукта — понимает ли модель свободное решение ребёнка (R60). Поле ответа отвечало на вопрос попроще: прочтёт ли модель короткую надпись в рамке. Без поля модель сама отличает итог от промежуточных чисел, пометок и зачёркнутого, а ребёнок пишет, как в тетради. Геометрия — один источник, потому что сервер принимает картинку только точного размера (spec 4.3) и переводит рамку модели в координаты листа (R43): лист клиента, который хоть на единицу отличается от листа сервера, ломал бы и то и другое.
+
+— *Цена:* модель может принять за итог промежуточное число, пометку или ключ, записанный по ходу решения. Это меряет eval: «ответ найден» — рамка итога против штрихов, которые автор пометил при письме (T73, T74). Сценарии живого прогона нарочно ставят ответ не последним, среди кандидатов, а ключ — промежуточным числом (T75).
+
+— *Отвергнуто:* поле Answer и кнопка Check (R38) — решение автора.
+
+Поправляет R38 и R42. — product 5; spec 4, 5, 20 (Т-16); privacy 5; RUN.md (T48, T67, T71).
+
+**R62. Один вызов Gemini разбирает весь лист и видит ключ; вердикт выносит программа по итогу, прочитанному дважды.** Решение автора 2026-10-15; сверка итога и контрольные варианты eval — план E2, утверждён тогда же.
+1. **Вход** — картинка листа, условие и класс задачи, её ключ и решение. Ничего о человеке.
+2. **Выход** — итоговый ответ, разборчив ли он и однозначен ли; ход рассуждения по шагам; первая ошибка с `box_2d`; один наводящий вопрос; своя оценка модели.
+3. **Итог — дважды:** `written` — как написан, с меткой, единицей и `=`; `value` — голое значение. Ещё — другие кандидаты и «ответа нет».
+ - Программа принимает `value`, только если он виден в `written`, а остальное в `written` — метка ответа, единица задачи или `=`. Иначе — `UNREADABLE`.
+ - Другой кандидат с иным значением и «ответа нет» — свои причины `UNREADABLE` с подсказкой «Circle your answer».
+4. **Вердикт выносит программа:** нормализует принятый `value` и сравнивает его с ключом, как в R38 (spec 5). Своя оценка модели вердикт не решает (R37).
+5. **Результат:**
+ - верно — «Correct! I read “7”.» и решение;
+ - неверно — «Not yet. I read “6”.», рамка по штрихам ребёнка (R43) и вопрос, прошедший проверку утечки; не прошёл — текст ловушки или шаблон (R24). Ответ задачи не показывается;
+ - неразборчиво или сбой — Send again: это не вердикт;
+ - затем — Next task. Лестницы подсказок нет (R39).
+6. **У eval два обязательных контрольных варианта промпта** (R65):
+ - без ключа — модель ещё и решает задачу сама. Это мера понимания задачи и того, насколько ключ сдвигает чтение;
+ - «только чтение» — первый вызов двухвызовной схемы R38: его цена и задержка.
+
+Ещё: кеш Gemini для проекта выключает ресурс Terraform `google_vertex_ai_cache_config`, а не рецепт, как писал spec 15.2 (privacy 5.2). Ресурс есть в провайдере `google` 8.3.0: это одиночка на проект, и `disable_cache = true` выключает кеш (документация ресурса в репозитории провайдера на теге v8.3.0, прочитана 2026-10-16). Какие права ему нужны и как он ведёт себя на живом проекте, проверяет T64; если не подойдёт — шаг рецепта.
+
+— *Почему:*
+- Один вызов — кратчайший путь к цифрам E2, и он дешевле и быстрее двух (R39, п. 5).
+- Ключ во входе — отступление от R38, который отверг его, чтобы модель не «дочитывала» ожидаемый ответ. Решение автора — мерить, а не гадать: вариант eval без ключа показывает, насколько ключ сдвигает чтение. Разбор листа видит ключ и так: без него место ошибки в свободном решении не найти (R39).
+- В одном вызове ключ видит и чтение, поэтому довод R39 «вызов 1 ключа не видит» в E2 не держится. Его место занимает сверка `value` с `written`: модель не может вернуть значение, которого нет в её же прочтении записи, — например, ключ при написанной «6». «Дочитать» саму запись она может; это и меряет вариант без ключа.
+- Вердикт программы сохраняет главное в R37: модель не делает верный ответ неверным своей оценкой.
+
+— *Цена:* опаснее всего ложный `CORRECT` — модель видит ключ в неверной записи. Его меряют пара вариантов eval, с ключом и без, и метрика «описка не исправлена к ключу» (T74).
+
+— *Следствие:* один вызов или два в бете, модель, настройки и тайм-аут решают выводы E2 (T76).
+
+— *Отвергнуто:*
+- вердикт по оценке модели — как в R37;
+- `value` без сверки с `written`: модель могла бы молча «дочитать» ключ.
+
+Поправляет R38 и R39; R37 и R43 в силе. — product 5.5, 5.6; spec 5, 7, 10.3–10.6, 15.2; privacy 5; RUN.md (T35, T66, T69, T74).
+
+**R66. Слать ли штрихи вместо PNG — исследование E2 (T77): автор открыл заново отвергнутое в R42.** Решение автора 2026-10-15.
+- До исследования на сервер уходит PNG листа, как велит R42 (R61).
+- Журнал автора хранит и штрихи как есть — точки со временем, тип указателя, нажим и наклон, — чтобы исследованию не понадобилось менять приложение (R65). Живое приложение модели их не шлёт: штрихи уходят в Gemini только в eval T77, с образцами автора, — eval-наборы и так отдельная обработка, которая идёт через Gemini (privacy 4).
+- T77 сравнивает на этом журнале варианты eval: PNG, нарисованный на сервере из штрихов; штрихи текстом с порядком написания, без картинки; картинка и порядок написания вместе. Метрики — те же, что у eval, плюс объём запроса, токены и задержка. Рендер на сервере идёт только в eval: в сервис он не входит.
+- Что SPA шлёт на проверку в бете, T77 записывает решением по R42.
+
+— *Почему:* решение автора. R42 отверг штрихи векторами и рендер на сервере, потому что рендер ушёл вместе с путём Б (R37). Это довод об устройстве сервиса, а не о качестве: штрихи и картинку на образцах не сравнивали. А в штрихах есть то, чего нет в картинке, — порядок написания, и итог обычно написан последним (R61).
+
+— *Цена:* штрихи со временем и нажимом лежат в журнале на устройстве автора. Это собственные образцы автора, и в репозиторий они не попадают (R65).
+
+— *Отвергнуто:* менять, что уходит на сервер, до исследования: рендер вошёл бы в сервис раньше, чем цифры покажут, нужен ли он.
+
+Поправляет R42. — spec 4, 7; privacy 1, 5; RUN.md (T72, T77).
+
## ИИ-учитель
-**R22. Модели Gemini — актуальные GA-версии с закреплённым ID.** Ответ читает модель класса Flash-Lite или Flash (вызов 1, R38), черновик разбирает Flash (вызов 2, R39). Gemini 2.5 Flash не используем. — *Почему:* по release notes Vertex AI Gemini 2.5 Flash снимают 16.10.2026. Модели обновляются примерно раз в месяц, поэтому смена версии идёт через eval-наборы чтения и разбора. Конкретные ID выбираются по спайкам S1 и S2 (Т-5). — spec 7.1. — *Поправка 2026-10-11:* роли моделей распределены по вызовам R38 и R39, а не по схеме «текст — путь Б».
+**R22. Модели Gemini — актуальные GA-версии с закреплённым ID.** Ответ читает модель класса Flash-Lite или Flash (вызов 1, R38), черновик разбирает Flash (вызов 2, R39). Gemini 2.5 Flash не используем. — *Почему:* по release notes Vertex AI Gemini 2.5 Flash снимают 16.10.2026. Модели обновляются примерно раз в месяц, поэтому смена версии идёт через eval-наборы чтения и разбора. Конкретные ID выбираются по спайкам S1 и S2 (Т-5). — spec 7.1. — *Поправка 2026-10-11:* роли моделей распределены по вызовам R38 и R39, а не по схеме «текст — путь Б». — *Поправка 2026-10-15:* модели выбирает eval E2, а не спайки S1 и S2. `just eval` прогоняет журнал автора на Flash-Lite, Flash и Pro, с разными настройками картинки и размышлений (R65), а модель, настройки и один вызов или два для беты — то есть Т-5 — решают выводы E2 (T76). В живом приложении E2 один вызов (R62), по умолчанию — Flash. ID по-прежнему точный.
**R23. Вердикт приходит сразу, подсказка — следом, и всё это внутри одного запроса.** Порядок работы canvas-api:
1. сразу после чтения ответа и сравнения с ключом публикует в Centrifugo событие `verdict`;
2. при неверном ответе разбирает черновик и публикует `hint` с рамкой;
3. HTTP-ответ приходит последним и повторяет все события на случай разрыва WebSocket.
-Записи и публикации после взятия аренды идут с её токеном (R42): проверка, потерявшая аренду, ничего не публикует. Лимит выходных токенов взят с запасом, потому что токены размышлений входят в него. — *Почему:* вердикт должен появиться за 1–2 секунды, а разбор черновика идёт дольше. При оплате Cloud Run за запросы CPU выделяется только на время обработки запроса, и фоновая работа после ответа почти не получает процессора. Поэтому всё остаётся внутри запроса. — *Отвергнуто:* горутина после ответа; постоянный CPU на инстансе (дороже); Cloud Tasks для подсказки (лишний компонент). — spec 7.1, 8.4, 9.4. — *Поправка 2026-10-11:* вердикт публикуется после чтения и сравнения (R38), а не после движка; подсказка — после разбора черновика (R39). Сверку `revision` строки заменил токен аренды проверки (R42).
+Записи и публикации после взятия аренды идут с её токеном (R42): проверка, потерявшая аренду, ничего не публикует. Лимит выходных токенов взят с запасом, потому что токены размышлений входят в него. — *Почему:* вердикт должен появиться за 1–2 секунды, а разбор черновика идёт дольше. При оплате Cloud Run за запросы CPU выделяется только на время обработки запроса, и фоновая работа после ответа почти не получает процессора. Поэтому всё остаётся внутри запроса. — *Отвергнуто:* горутина после ответа; постоянный CPU на инстансе (дороже); Cloud Tasks для подсказки (лишний компонент). — spec 7.1, 8.4, 9.4. — *Поправка 2026-10-11:* вердикт публикуется после чтения и сравнения (R38), а не после движка; подсказка — после разбора черновика (R39). Сверку `revision` строки заменил токен аренды проверки (R42). — *Поправка 2026-10-15:* в E2 Centrifugo нет (R60): вердикт, рамка и вопрос приходят одним HTTP-ответом, после единственного вызова (R62). Всё по-прежнему делается внутри запроса — довод о процессоре после ответа в силе. Centrifugo и события `verdict` и `hint` возвращаются в E15 (R41).
**R24. Если LLM не справился, выручают запасные подсказки; очевидные утечки ответа ловит программа.** Запасная подсказка зависит от ступени:
- на ступенях 1–2 — текст ловушки, который автор задачи написал для этого неверного ответа, а если такого нет — шаблон ступени;
@@ -746,7 +897,7 @@ Centrifugo сохраняет роль из R23. Событие `verdict` ухо
- текст нормализуется, числа словами переводятся в цифры, числа сравниваются отдельными токенами;
- тексты автора проходят ту же проверку при старте сервиса, поэтому запасная подсказка проходит её всегда.
-Лестница подсказок идёт по числу неверных ответов на холсте (R39). После третьей ступени наставник предлагает позвать взрослого. — *Почему:* ребёнок всегда получает реакцию, даже при тайм-ауте, сбое или браке модели. Разбор черновика видит ключ (R39), поэтому проверку проходит каждая подсказка модели. Проверка ловит очевидные утечки, но не доказывает их отсутствие. Перефразированную утечку ловят промпт, eval-набор и ручной разбор, и доля утечек — метрика рубрики. Цвет подсветки выбирает клиент по типу действия, а не модель. — *Отвергнуто:* буквальное сравнение строк без нормализации: оно пропускало «minus five» и запрещало собственный пример третьей ступени. — spec 7.2, 7.3. — *Поправка 2026-10-11:* шаблоны по коду ошибки и пример третьей ступени от движка заменены текстами автора задачи (R40) и шаблонами ступеней. Лестница больше не опирается на ключ «код ошибки + место»: её ведёт счёт неверных ответов (R39).
+Лестница подсказок идёт по числу неверных ответов на холсте (R39). После третьей ступени наставник предлагает позвать взрослого. — *Почему:* ребёнок всегда получает реакцию, даже при тайм-ауте, сбое или браке модели. Разбор черновика видит ключ (R39), поэтому проверку проходит каждая подсказка модели. Проверка ловит очевидные утечки, но не доказывает их отсутствие. Перефразированную утечку ловят промпт, eval-набор и ручной разбор, и доля утечек — метрика рубрики. Цвет подсветки выбирает клиент по типу действия, а не модель. — *Отвергнуто:* буквальное сравнение строк без нормализации: оно пропускало «minus five» и запрещало собственный пример третьей ступени. — spec 7.2, 7.3. — *Поправка 2026-10-11:* шаблоны по коду ошибки и пример третьей ступени от движка заменены текстами автора задачи (R40) и шаблонами ступеней. Лестница больше не опирается на ключ «код ошибки + место»: её ведёт счёт неверных ответов (R39). — *Поправка 2026-10-15:* в E2 ступеней нет (R62): запасной текст — ловушка автора для этого неверного ответа, а если её нет — шаблон ступени 1. Случая попроще нет, и задачи E2 его не несут (R63). Проверка запрещённых фрагментов — без изменений.
**R39. После неверного ответа вызов 2 разбирает черновик вместе с ключом и задаёт один вопрос; лестница — по числу неверных ответов.** Вызов 2 — Gemini — идёт только после `INCORRECT`, если выполнено всё:
- неверных ответов на холсте не больше трёх;
@@ -788,7 +939,7 @@ Centrifugo сохраняет роль из R23. Событие `verdict` ухо
- лестница по коду ловушки: у неузнанного ответа кода нет, а дети чередуют ответы;
- показывать ответ после третьей ступени (product 5.4).
-Заменяет R21, поправляет R24. — product 5.3, 5.4; spec 6, 7, 10.5, 10.6; privacy 5, 10.
+Заменяет R21, поправляет R24. — product 5.3, 5.4; spec 6, 7, 10.5, 10.6; privacy 5, 10. — *Поправка 2026-10-15:* в E2 второго вызова и лестницы нет. Лист разбирает тот же единственный вызов, что читает итог (R62), а после `INCORRECT` ребёнок получает рамку и один вопрос, прошедший проверку утечки, или запасной текст ступени 1 (R24). Счёта неверных ответов, «решить заново» и случая попроще в E2 нет. Разбор по-прежнему видит ключ, а вердикт выносит программа, но довод п. 1 «вызов 1 ключа не видит» в E2 заменяет сверка итога (R62). Один вызов или два в бете решают выводы E2 (T76). Пункт «Отвергнуто» о черновике в Gemini при каждой проверке, в том числе при верном ответе, в E2 не действует: лист один, и он уходит на каждую проверку (R61, R62). Что из этого останется в бете, тоже решают выводы E2.
## Данные, доступ и эксплуатация
@@ -876,7 +1027,27 @@ Centrifugo сохраняет роль из R23. Событие `verdict` ухо
- проверка с рамками, но без вопросов наставника;
- проверка с шаблонными вопросами вместо модели.
-— spec 5.4, 8.4, 14.5; product 5.6, 9. — *Поправка 2026-10-11:* строк, стрелок и Mathpix больше нет. Лимит теперь считают два счётчика — чтений и разборов (R38, R39). Фраза показывается на каждое нажатие Check: проверки по паузе больше нет. Подтверждать ли верный ответ, когда кончились разборы, но остались чтения, решим после MVP — в эпике тарифов (E7 в RUN.md).
+— spec 5.4, 8.4, 14.5; product 5.6, 9. — *Поправка 2026-10-11:* строк, стрелок и Mathpix больше нет. Лимит теперь считают два счётчика — чтений и разборов (R38, R39). Фраза показывается на каждое нажатие Check: проверки по паузе больше нет. Подтверждать ли верный ответ, когда кончились разборы, но остались чтения, решим после MVP — в эпике тарифов (E7 в RUN.md). — *Поправка 2026-10-15:* в E2 лимитов на ребёнка и родителя нет: нет ни детей, ни базы (R60). Вместо них — дневной потолок вызовов Gemini, счётчик в памяти экземпляра (R64). Резерв, его возврат и счёт сделанного вызова — как здесь; исчерпан — `UNCHECKED` · `limit`, и модель не зовётся. Лимиты этой записи возвращаются в E15.
+
+**R65. Замер E2: панель автора, журнал на его устройстве и eval; разбор модели целиком — исключение из «ответ скрыт до решения» на время E2; образцы — вне репозитория.** Решение автора 2026-10-15; форма журнала, выгрузки и лога — план E2, утверждён тогда же.
+- **Панель автора** после каждой проверки показывает время — клиента, сервера по этапам и Gemini, — холодный старт, токены, цену, модель и `prompt_version`, а ещё разбор модели целиком: шаги, итог, кандидаты, первую ошибку, вопрос и свою оценку модели.
+- **Разметка автора** ставится до отправки и при письме, а не задним числом по рамке модели: так она не подстраивается под ответ модели. Панель можно скрыть — так собирается тестовая часть образцов.
+- **Журнал** — в IndexedDB на устройстве автора: PNG листа, штрихи как есть (R66), разметка, ответ сервера целиком, времена и версии. Сервер ничего не хранит: базы в E2 нет (R60).
+- **Выгрузка журнала** — архив по версионированному контракту из `proto/`. Go разбирает её пакетом, который читает eval, и этот разбор проверяет fuzz-тест.
+- **`just eval`** (`cmd/eval`) прогоняет выгрузки тем же ходом проверки на Flash-Lite, Flash и Pro, с разными настройками картинки и размышлений и с вариантами промпта (R62). Учётные данные — ADC автора; в образ сервера `cmd/eval` не входит. В живом приложении по умолчанию — Flash (R22).
+- **Образцы, выгрузки и отчёты eval** лежат в `samples/`, а его держат вне git и вне сборки `.gitignore` и `.dockerignore`: строки в них добавляет задача журнала (T72). Почерк образцов в репозиторий не попадает.
+- **Лог сервера** — событие проверки по таблице «событие → разрешённые поля» с тестом аудита, как в образце (standalone: R141, R169): ID, коды, тайминги, токены, цена, модель, `prompt_version` — без прочитанного ответа, текстов и картинок.
+- **Исключение.** Ответ проверки несёт разбор модели целиком, а модель видела ключ (R62) и может его назвать. Правило «ответ скрыт до решения» (spec 8.6, 13) в E2 действует для всего, кроме этого разбора на панели автора.
+
+— *Почему:* решение автора. Цель E2 — увидеть, как модель поняла решение, и узнать задержку и цену (R60): для этого нужен разбор целиком, а не только вердикт. Ребёнку исключение ответа не открывает: детей в E2 нет, а доступ — по ключу автора (R64). Журнал живёт на устройстве, потому что базы нет. Образцы — вне репозитория, потому что eval-наборы — отдельная обработка и хранятся вне репозитория и `testdata/` (privacy 4).
+
+— *Цена:* Д17 — в детском режиме ребёнок увидел бы ответ до решения. В E15 разбор не уходит детскому экрану: он нужен только панели автора и eval.
+
+— *Отвергнуто:*
+- панель только с тем, что увидит ребёнок, — вердиктом и вопросом: по ним не понять, где модель поняла решение, а где нет;
+- журнал на сервере: базы в E2 нет, а образцы нужны одному автору.
+
+— product 9, 10, 12; spec 7.4, 8.6, 13, 16, 17; privacy 4, 5.3; RUN.md (T70, T72–T75); R22.
## Документация
@@ -898,7 +1069,7 @@ Centrifugo сохраняет роль из R23. Событие `verdict` ухо
- удалить прежнюю разбивку и ссылаться на историю git;
- вынести прежнюю разбивку в отдельный файл.
-— RUN.md. — *Поправка 2026-10-15:* сделанный эпик уходит из RUN.md в архив `docs/plan/`, и туда же после разбиения E2 уходит остаток прежней разбивки (R59). До разбиения она остаётся в RUN.md: это сырьё для ближайшего планирования.
+— RUN.md. — *Поправка 2026-10-15:* сделанный эпик уходит из RUN.md в архив `docs/plan/`, и туда же после разбиения E2 уходит остаток прежней разбивки (R59). До разбиения она остаётся в RUN.md: это сырьё для ближайшего планирования. — *Поправка 2026-10-15 (разбиение E2):* E2 разбит на задачи 2026-10-15, но не как MVP, а как минимальный срез с замером Gemini (R60). Бета — теперь эпик E15, первый в таблице 2. Прежняя разбивка T13–T58 ушла в архив `docs/plan/E2-draft.md` (R59) и остаётся сырьём — теперь для разбиения E15.
**R59. Сделанный эпик уходит из RUN.md в архив `docs/plan/<эпик>.md`.**
- В архив дословно переезжает всё, что RUN.md держал об эпике: описание («Результат», «Нужно», «Почему такой порядок»), таблица задач и тела задач с «Сделано» и «Открыто». Первый архив — `docs/plan/E1.md`.
@@ -913,4 +1084,4 @@ Centrifugo сохраняет роль из R23. Событие `verdict` ухо
- один архивный файл на все эпики: он растёт так же, а файл на эпик закрыт и больше не меняется;
- переписать ссылки «RUN.md, Txx» в журнале и документах: номер задачи и так однозначен, а записи журнала правятся только поправками.
-Поправляет R44. — RUN.md («Как устроен план: эпики»); `docs/plan/`.
+Поправляет R44. — RUN.md («Как устроен план: эпики»); `docs/plan/`. — *Поправка 2026-10-15 (разбиение E2):* архив черновика держит всё приложение дословно, а у шести задач, которые E2 взял себе (T16, T18, T19, T34, T35, T48), — ещё и их текст до разбиения, под заголовком вида «#### T16 (до разбиения E2).». Так поиск по номеру находит одну задачу, а E15 видит исходный текст: перо, ладонь, лестницу. Это отступление от «задачи, которые не пережили разбиение» выше. Остаток черновика — не только история, как сказано в «Почему», но и сырьё для разбиения E15: его задачи переносятся туда только после сверки с R37–R43 и выводами E2. Задачи черновика, которые пережили разбиение, сохранили номера; новые получают свободные, с T61.
diff --git a/docs/privacy.md b/docs/privacy.md
index 8ce2b82..b93c72b 100644
--- a/docs/privacy.md
+++ b/docs/privacy.md
@@ -1,9 +1,11 @@
# Приватность и защита данных детей
-> Статус: черновик для обсуждения, 2026-09-27. Собран из блока о приватности в исходном черновике автора (в репозиторий не входит), с поправками, и исправлен по итогам ревью документов. Пересмотрен 2026-10-11 при повороте к LLM-first (R37–R43).
+> Статус: черновик для обсуждения, 2026-09-27. Собран из блока о приватности в исходном черновике автора (в репозиторий не входит), с поправками, и исправлен по итогам ревью документов. Пересмотрен 2026-10-11 при повороте к LLM-first (R37–R43) и 2026-10-16 под эпик E2 (R60–R67).
>
> Документ описывает, какие данные мы собираем, где их храним, кому передаём, как удаляем и какие законы на это влияют. Обещания родителям — в [product.md](product.md), раздел 11. Технические меры — в [spec.md](spec.md), разделы 12–13. Открытые вопросы (П-…) — в разделе 12.
>
+> Всё это — о закрытой бете, эпике E15. До неё идёт E2, срез с замером Gemini (R60). В E2 детей нет: пользуется только автор, по ключу доступа (R64), и на листах — его собственный почерк. Что E2 отправляет в Gemini и где лежат образцы автора — разделы 4 и 5.1.
+>
> **Это не юридическая консультация.** Перед публичным запуском документ проверяет юрист. Закрытая бета со знакомыми семьями — не замена такой проверке. Законы о данных детей не делают исключения для знакомых: договоры с обработчиками, согласие и, для ЕС и Великобритании, DPIA нужны до того, как данные первого ребёнка начнут обрабатываться (П-2, П-3).
## 1. Принципы
@@ -15,8 +17,8 @@
- Поэтому обещания «полной анонимности» и «Zero-PII» из черновика мы не используем. Мы говорим честно: данных минимум, и они псевдонимные.
- **Контроль у родителя.** Родитель даёт согласие, видит данные и удаляет профиль или аккаунт.
- **Приватность по умолчанию** (UK AADC). Нет публичных профилей, общения между пользователями, геолокации, рекламы и профилирования в маркетинговых целях.
-- **Почерк остаётся на устройстве.** Штрихи черновика и поля ответа — с координатами, временем и нажимом — хранятся только на устройстве, в IndexedDB. По нажатию Check на сервер приходят две картинки, нарисованные из этих штрихов. Сервер держит их только в памяти запроса и нигде не сохраняет (R42).
-- **Внешний ИИ видит почерк, но мы не сообщаем ему, чей он.** Картинку поля ответа Gemini получает на каждое нажатие Check, если поле не пусто и не исчерпан дневной лимит, а картинку черновика — только после неверного ответа (R38, R39). На картинке может оказаться всё, что ребёнок написал на листе, в том числе имя (раздел 5.3). Сами мы к картинкам ничего не добавляем: ни идентификаторов, ни псевдонима, ни данных родителя (раздел 5).
+- **Почерк остаётся на устройстве.** Штрихи листа — с координатами, временем и нажимом — хранятся только на устройстве, в IndexedDB. Отдельного поля ответа нет: решение и итоговый ответ ребёнок пишет на одном листе (R61). По нажатию Send Answer на сервер приходит одна картинка — PNG листа, нарисованный из этих штрихов. Сервер держит её только в памяти запроса и нигде не сохраняет (R42).
+- **Внешний ИИ видит почерк, но мы не сообщаем ему, чей он.** Картинку листа Gemini получает на каждое нажатие Send Answer, если лист не пуст и не исчерпан дневной лимит (R61, R62). На картинке может оказаться всё, что ребёнок написал на листе, в том числе имя (раздел 5.3). Сами мы к картинкам ничего не добавляем: ни идентификаторов, ни псевдонима, ни данных родителя (раздел 5).
- **Честные сроки.** Удаление описано с учётом резервных копий, журналов и других устройств родителя (раздел 7), а не как «очистка за 100 мс» из черновика.
## 2. Правовые режимы
@@ -37,7 +39,7 @@
- **Ребёнок** — субъект данных. Своего аккаунта у него нет (R07).
- **Мы** — оператор (controller).
- **Обработчики (processors):**
- - Google: Firebase Auth, App Check и reCAPTCHA Enterprise, Firebase Hosting, Cloud Run, Cloud Logging, Cloud Trace, Secret Manager, Gemini Enterprise Agent Platform (бывший Vertex AI) — на нём Gemini читает ответ и разбирает черновик;
+ - Google: Firebase Auth, App Check и reCAPTCHA Enterprise, Firebase Hosting, Cloud Run, Cloud Logging, Cloud Trace, Secret Manager, Gemini Enterprise Agent Platform (бывший Vertex AI) — на нём Gemini читает ответ и разбирает решение на листе;
- Neon — база данных;
- Upstash — история событий Centrifugo.
@@ -51,22 +53,24 @@
| Запись согласия: статус, версия уведомления, метод, время, кто подтвердил | родителя | Neon, `parents` | доказательство согласия | до удаления аккаунта (дольше — П-7) | нет |
| Подписанная форма согласия | родителя | почта или хранилище оператора, вне сервиса | доказательство согласия | по П-7 | нет |
| Хеш PIN детского режима, счётчик попыток | родителя | Neon, `parents` | выход из детского режима | до удаления аккаунта | нет |
-| Псевдоним, аватар, класс | ребёнка | Neon, `children` | профиль | до удаления профиля | нет: в разбор черновика уходит класс задачи, а не класс из профиля (R39) |
+| Псевдоним, аватар, класс | ребёнка | Neon, `children` | профиль | до удаления профиля | нет: в разбор решения уходит класс задачи, а не класс из профиля (R39) |
| UUID ребёнка и холстов | ребёнка | Neon, OpenFGA (кортежи и `changelog`), логи, трассы Cloud Trace, мета-ключи истории Centrifugo в Upstash | связь данных, права | до удаления профиля; `changelog` — до суток после удаления; в логах и трассах — 30 дней; мета-ключи — 1 час после последнего события | нет |
-| Штрихи черновика и поля ответа: координаты, время, нажим | ребёнка | только устройство (IndexedDB) | черновик и ответ; картинки проверки; привязка рамки ошибки к штрихам (R43) | до решения холста или нового холста той же задачи; 30 дней без изменений; до выхода родителя или удаления профиля и аккаунта (R42) | нет: уходят только картинки, нарисованные из штрихов |
-| Картинки проверки: PNG поля ответа и черновика | ребёнка | на сервере — только память запроса canvas-api: ни базы, ни журналов, ни трасс (R42); на устройстве — IndexedDB, до ответа на проверку; если ответа нет, а запись изменилась, — до этого изменения, и не дольше черновика | чтение ответа, разбор черновика; повтор после разрыва ([spec.md](spec.md), раздел 3.1) | на сервере — время запроса; на устройстве — до ответа | ответ — в Gemini на каждую проверку, если поле не пусто и не исчерпан лимит; черновик — только после неверного ответа (раздел 5.1) |
-| Проверки: прочитанный ответ, вердикт и его причина, совпавшая ловушка | ребёнка | Neon, `checks` | проверка, прогресс, ответ на повтор той же проверки без нового вызова Gemini (R42) | пока существует профиль (П-6) | прочитанный ответ и ловушка — в разбор черновика этого же холста |
-| Подсказки: текст, рамка, ступень, источник | ребёнка | Neon, `hints` | прогресс, контроль качества, ответ на повтор той же проверки | пока существует профиль | прошлые вопросы холста — в следующий разбор его черновика |
+| Штрихи листа: координаты, время, нажим | ребёнка | только устройство (IndexedDB) | решение и ответ; картинка проверки; привязка рамки ошибки к штрихам (R43) | до решения холста или нового холста той же задачи; 30 дней без изменений; до выхода родителя или удаления профиля и аккаунта (R42) | нет: уходит только картинка, нарисованная из штрихов. Слать ли штрихи, решает исследование E2 (R66) |
+| Картинка проверки: PNG листа | ребёнка | на сервере — только память запроса canvas-api: ни базы, ни журналов, ни трасс (R42); на устройстве — IndexedDB, до ответа на проверку; если ответа нет, а запись изменилась, — до этого изменения, и не дольше листа | чтение ответа, разбор решения; повтор после разрыва ([spec.md](spec.md), раздел 3.1) | на сервере — время запроса; на устройстве — до ответа | да: в Gemini на каждую проверку, если лист не пуст и не исчерпан лимит (раздел 5.1) |
+| Проверки: прочитанный ответ, вердикт и его причина, совпавшая ловушка | ребёнка | Neon, `checks` | проверка, прогресс, ответ на повтор той же проверки без нового вызова Gemini (R42) | пока существует профиль (П-6) | прочитанный ответ и ловушка — в разбор решения этого же холста |
+| Подсказки: текст, рамка, ступень, источник | ребёнка | Neon, `hints` | прогресс, контроль качества, ответ на повтор той же проверки | пока существует профиль | прошлые вопросы холста — в следующий разбор его решения |
| Дневные счётчики | ребёнка и родителя | Neon, `usage_daily`, `parent_usage_daily` | лимиты | 30 дней | нет |
| События холста: вердикт с прочитанным ответом, вопрос с рамкой | ребёнка | Upstash (история Centrifugo) | доставка и восстановление после разрыва | 10 минут; мета-ключ канала — 1 час | нет |
| IP-адрес, user agent | устройства | журналы запросов Cloud Run (в `us-east4`, R57); журналы Firebase Hosting; оценка риска reCAPTCHA Enterprise (App Check) | безопасность, защита от злоупотреблений | 30 дней в Cloud Logging (П-9) | нет |
-| **Eval-наборы почерка** (отдельная обработка, не продукт) | взрослых; детей — только по П-10 | закрытое хранилище оператора, вне репозитория и `testdata/` | качество чтения ответа и разбора черновика, спайки S1 и S2 | до конца исследования, не больше 12 месяцев | да: eval-набор прогоняется через Gemini при выборе и смене модели (R22) |
+| **Eval-наборы почерка** (отдельная обработка, не продукт) | взрослых; детей — только по П-10 | закрытое хранилище оператора, вне репозитория и `testdata/` | качество чтения ответа и разбора решения; выбор модели и настроек — eval E2 (R65) | до конца исследования, не больше 12 месяцев | да: eval-набор прогоняется через Gemini при выборе и смене модели (R22) |
+| **Образцы автора в E2:** PNG листа, штрихи как есть — точки со временем, тип указателя, нажим и наклон, — разметка автора, ответ сервера, времена и версии | автора — взрослого (R60) | журнал в IndexedDB на устройстве автора; выгрузки и отчёты eval — в `samples/`, вне git и сборки (R65) | замер E2 и eval-набор; исследование «картинка или штрихи» (R66) | решает автор: это его собственные образцы | да: PNG — при проверке и в eval; штрихи — только в исследовании, живое приложение их не шлёт (R66) |
- **Прочитанный ответ** хранится, только если у него нужная форма — это число или вариант из списка, не длиннее 32 символов ([spec.md](spec.md), раздел 5). Прочитанное в другой форме — например, слово или имя вместо ответа — не попадает ни в базу, ни в события, ни в журналы. Ребёнок видит просьбу переписать.
- **Журналы и трассы приложения** не содержат прямых идентификаторов, картинок, прочитанного ответа и текста подсказок ([spec.md](spec.md), раздел 16). В них есть UUID — псевдонимные данные, они учтены в реестре.
- **Платформенные журналы** запросов Cloud Run и Firebase Hosting сами пишут IP-адрес и user agent — это учтено в реестре и в П-9.
- **Где лежат журналы и трассы.** Журналы проекта — в бакете `default-us-east4`, трассы — в `us-east4` (R57). В `global` остаётся только аудит изменений самого проекта (`_Required`): кто и что поменял в облаке, без данных семей. Его место не меняется без организации Google Cloud.
- **Eval-наборы** — отдельная обработка со своей целью. Образцы хранятся у оператора и при каждом прогоне уходят в Gemini, поэтому детские образцы требуют отдельного решения (П-10). Пока его нет, eval-наборы собираются только из образцов взрослых, в том числе с нарочно «детским» почерком. Картинки из проверок в eval-наборы не попадают: сервис их не хранит (R42). В тестах и `testdata/` — только синтетические или взрослые данные.
+- **В E2** детей, родителей и базы нет (R60). Из строк беты в таблице выше в нём есть только журналы: запросов платформы — с IP-адресом и user agent, приложения — ID проверок, коды, тайминги, число токенов и цена, без прочитанного ответа, текстов и картинок (R65). Сервер ничего не хранит. Всё остальное, что E2 собирает, — образцы самого автора, у них своя строка: журнал проверок на его устройстве и выгрузки в `samples/`. Каталог `samples/` держится вне git и вне сборки образа, поэтому почерк образцов в репозиторий не попадает (R65).
## 5. Что получает внешний ИИ
@@ -74,24 +78,33 @@
### 5.1. Что уходит в Gemini
-- **Вызов 1 — чтение ответа** (R38). Идёт при каждой проверке, если поле ответа не пусто и не исчерпан дневной лимит. Получает:
- - PNG поля ответа;
+**E2 — один вызов** (R62). Идёт при каждой проверке, если лист не пуст и не исчерпан дневной потолок (R64). Получает:
+- PNG всего листа;
+- условие и класс задачи — из данных задачи;
+- ключ и эталонное решение.
+
+Ничего о человеке в нём нет: ни идентификаторов, ни данных автора. На листе — почерк самого автора: детей в E2 нет (R60). Тот же вход — с ключом, без него или только для чтения ответа — получают прогоны eval на образцах автора (R62, R65); штрихи вместо картинки уходят модели только в исследовании, а не из живого приложения (R66).
+
+**Бета (E15)** — один вызов, как в E2, или два, как ниже, — решают выводы E2 (T76); тогда раздел переписывается. Ниже — схема из двух вызовов по R38 и R39, с одной картинкой листа вместо двух (R61):
+
+- **Вызов 1 — чтение ответа** (R38). Идёт при каждой проверке, если лист не пуст и не исчерпан дневной лимит. Получает:
+ - PNG листа;
- подсказку формы: вид ответа, единицу и подписи вариантов.
- Условия задачи, ключа и черновика в нём нет.
-- **Вызов 2 — разбор черновика** (R39). Идёт только после неверного ответа, если неверных ответов на холсте не больше трёх и черновик не пуст. Получает:
- - PNG всего черновика;
+ Условия задачи и ключа в нём нет.
+- **Вызов 2 — разбор решения** (R39). Идёт только после неверного ответа, если неверных ответов на холсте не больше трёх. Получает:
+ - PNG листа;
- условие и класс задачи — из данных задачи, а не из профиля ребёнка;
- ключ и эталонное решение;
- прочитанный ответ ребёнка и совпавшую ловушку автора;
- ступень подсказки и прошлые вопросы этого холста.
- **Ни в одном вызове нет** UUID ребёнка и холста, Firebase UID, псевдонима, аватара, класса из профиля и данных родителя.
-- **Картинки рисует клиент из штрихов линиями постоянной толщины,** поэтому ни время, ни нажим в них не попадают. Сервер перекодирует каждую картинку, и метаданные файла до Gemini не доходят ([spec.md](spec.md), раздел 4).
+- **Картинку рисует клиент из штрихов линиями постоянной толщины,** поэтому ни время, ни нажим в неё не попадают. Сервер перекодирует картинку, и метаданные файла до Gemini не доходят ([spec.md](spec.md), раздел 4). Так и в E2.
### 5.2. Условия Gemini (Gemini Enterprise Agent Platform)
- **Обучение.** По разделу «Training Restriction» Service Specific Terms Google не использует данные клиента для обучения и дообучения моделей без его разрешения или указания. Это касается и картинок.
-- **Кеширование.** По умолчанию модели Google кешируют входы и выходы в памяти — до 24 часов, отдельно для каждого проекта. Кеширование отключается на уровне проекта — **отключаем**.
+- **Кеширование.** По умолчанию модели Google кешируют входы и выходы в памяти — до 24 часов, отдельно для каждого проекта. Кеширование отключается на уровне проекта — **отключаем** до первого вызова Gemini: ресурсом Terraform, а если на живом проекте он не подойдёт, — шагом рецепта выкладки (R62; [spec.md](spec.md), раздел 15.2).
- **Журналирование злоупотреблений.** По п. 4.3 условий Google Cloud Platform Google может журналировать промпты, чтобы выявлять нарушения правил использования. Срок хранения таких журналов на странице Google о нулевом хранении не назван. Исключение для режима нулевого хранения можно запросить — выясняем и оформляем (П-3). Пока исключения нет, картинки почерка могут попасть в эти журналы.
- **Не используем** функции, которые хранят данные:
- grounding через Google Search и Google Maps;
@@ -105,12 +118,12 @@
### 5.3. Что ребёнок может написать сам
-На листе можно написать что угодно, в том числе своё имя. Поле ответа Gemini видит на каждое нажатие Check, если оно не пусто и не исчерпан лимит, а черновик — после неверного ответа. Как это ограничено:
+На листе можно написать что угодно, в том числе своё имя. Лист Gemini видит на каждое нажатие Send Answer, если он не пуст и не исчерпан лимит (раздел 5.1). Как это ограничено:
- сервис ничего не добавляет к картинкам: ни имени, ни идентификаторов (раздел 5.1);
- картинки не хранятся ни у нас (R42), ни в кеше Gemini (раздел 5.2); журналы злоупотреблений — П-3;
-- прочитанное в поле ответа сохраняется, только если это ответ нужной формы (раздел 4). Имя, написанное вместо ответа, не сохраняется: ребёнок получит просьбу переписать;
+- прочитанный ответ сохраняется, только если это ответ нужной формы (раздел 4). Имя, написанное вместо ответа, не сохраняется: ребёнок получит просьбу переписать;
- уведомление для родителей просит не писать на листе имя и ничего личного;
-- надпись на картинке не меняет вердикт: его выносит программа по ключу, а чтение ключа не видит (R38). Надпись, похожая на указание модели, может увести от задачи вопрос разбора. Проверки вопроса ловят не всё, поэтому в eval-наборе разбора есть такие надписи, написанные взрослыми (раздел 10).
+- надпись на картинке не меняет вердикт: его выносит программа по ключу. В двух вызовах чтение ключа не видит (R38); в одном вызове E2 ключ видит и чтение, и программа принимает прочитанное значение, только если оно видно в записи (R62). Надпись, похожая на указание модели, может увести от задачи вопрос разбора. Проверки вопроса ловят не всё, поэтому в eval-наборе разбора есть такие надписи, написанные взрослыми (раздел 10).
## 6. Проверяемое согласие родителя
@@ -132,16 +145,16 @@
- **Что фиксируем:** статус, версию уведомления, метод, время и кто подтвердил. Они хранятся в `parents` ([spec.md](spec.md), раздел 11).
- **Что говорит уведомление:**
- какие данные собираем (раздел 4);
- - что картинки ответа и черновика уходят в Google Gemini и что на них видно всё, написанное на листе. Отсюда просьба не писать на листе имя и ничего личного (раздел 5.3);
+ - что картинка листа уходит в Google Gemini и что на ней видно всё, написанное на листе. Отсюда просьба не писать на листе имя и ничего личного (раздел 5.3);
- где хранятся и обрабатываются данные: в США — сервис и Gemini в Google Cloud, база в Neon (AWS), события в Upstash; все в Северной Вирджинии (R54);
- сроки хранения (раздел 8);
- - как удалить данные, в том числе черновики на устройствах. Отсюда совет выйти из аккаунта на устройстве, которое отдают или продают (раздел 7).
+ - как удалить данные, в том числе листы на устройствах. Отсюда совет выйти из аккаунта на устройстве, которое отдают или продают (раздел 7).
- **Взрослый ли пользователь.** Вход — только для взрослых 18+, это подтверждается при согласии (П-5).
## 7. Права родителя и удаление
**Права:**
-- просмотр данных ребёнка — прогресс в приложении, выгрузка по запросу на email поддержки; черновики видны на самом устройстве;
+- просмотр данных ребёнка — прогресс в приложении, выгрузка по запросу на email поддержки; листы видны на самом устройстве;
- исправление — редактирование профиля;
- удаление профиля ребёнка или всего аккаунта — в приложении;
- отзыв согласия — равнозначен удалению аккаунта.
@@ -154,15 +167,15 @@
| Neon, база `openfga` | кортежи ребёнка и его холстов; их записи в таблице `changelog` | кортежи — сразу; `changelog` — ежедневной очисткой, до суток; из PITR — через 6 часов после этого |
| Firebase Auth | аккаунт родителя, если удаляется аккаунт | сразу: отзыв токенов, затем `DeleteUser`; старые токены на других устройствах перестают работать |
| Upstash, история Centrifugo | события холстов; мета-ключ канала с UUID холста | события — через 10 минут, мета-ключ — через 1 час (`history_meta_ttl`) |
-| Устройство, где удаляли (IndexedDB) | черновики и поля ответа профиля, а при удалении аккаунта — всех профилей | сразу (R42) |
-| Другие устройства родителя (IndexedDB) | то же | когда там откроется приложение и узнает об удалении. Если приложение там больше не откроют, черновики останутся в браузере, пока их не сотрёт браузер или сам родитель |
+| Устройство, где удаляли (IndexedDB) | листы профиля, а при удалении аккаунта — всех профилей | сразу (R42) |
+| Другие устройства родителя (IndexedDB) | то же | когда там откроется приложение и узнает об удалении. Если приложение там больше не откроют, листы останутся в браузере, пока их не сотрёт браузер или сам родитель |
| Cloud Logging и Cloud Trace | UUID в журналах и трассах приложения, IP в журналах запросов | через 30 дней — срок хранения бакета журналов `default-us-east4` и трасс (R57, П-9) |
| Gemini | удалять нечего: кеш в памяти отключён | пока нет исключения по П-3, картинки и тексты проверок могут оставаться в журналах злоупотреблений Google; срок не назван (раздел 5.2) |
- Удаление требует свежего входа: PIN не подходит, ребёнок может его знать.
- Удаление идемпотентно. Сначала удаляются данные в Neon, потом права в OpenFGA, поэтому прерванное удаление повторный запрос доводит до конца ([spec.md](spec.md), 9.8, R27).
- Родитель получает подтверждение в приложении.
-- **Устройство, которым больше не пользуются.** Черновики на нём стираются, только когда там работает приложение: и по сроку, и после удаления. Поэтому уведомление советует выйти из аккаунта на устройстве, которое отдают или продают: выход стирает черновики сразу (R42).
+- **Устройство, которым больше не пользуются.** Листы на нём стираются, только когда там работает приложение: и по сроку, и после удаления. Поэтому уведомление советует выйти из аккаунта на устройстве, которое отдают или продают: выход стирает листы сразу (R42).
## 8. Политика хранения
@@ -170,13 +183,14 @@
| Данные | Цель | Срок | Как удаляется |
|---|---|---|---|
-| Штрихи черновика и поля ответа, на устройстве | черновик, картинки проверки, привязка рамки | до решения холста или нового холста той же задачи; 30 дней без изменений; до выхода родителя или удаления профиля и аккаунта (R42) | SPA — при этих событиях, а по сроку — при запуске |
-| Картинки проверки | чтение ответа, разбор черновика; повтор после разрыва | на сервере — время запроса; на устройстве — до ответа на проверку, а без ответа — пока запись не изменилась, и не дольше черновика ([spec.md](spec.md), раздел 9.2) | сервер не сохраняет (R42); SPA стирает, получив ответ |
+| Штрихи листа, на устройстве | решение и ответ, картинка проверки, привязка рамки | до решения холста или нового холста той же задачи; 30 дней без изменений; до выхода родителя или удаления профиля и аккаунта (R42) | SPA — при этих событиях, а по сроку — при запуске |
+| Картинка проверки | чтение ответа, разбор решения; повтор после разрыва | на сервере — время запроса; на устройстве — до ответа на проверку, а без ответа — пока запись не изменилась, и не дольше листа ([spec.md](spec.md), раздел 9.2) | сервер не сохраняет (R42); SPA стирает, получив ответ |
| Холсты, проверки, подсказки | прогресс, контроль качества | пока профиль активен; профиль ребёнка, по которому 12 месяцев не было активности родителя и ребёнка, удаляется автоматически, без отдельного уведомления. Этот срок заранее указан в уведомлении при согласии: email у нас нет, писать некуда (П-6) | ежедневное задание очистки (Cloud Scheduler → Cloud Run job) |
| Дневные счётчики | лимиты | 30 дней | задание очистки |
| Записи `changelog` OpenFGA | не нужны сервису | до суток | задание очистки |
| Неиспользованные инвайт-коды | доступ в бету | до `expires_at` | задание очистки |
-| Eval-наборы почерка | качество чтения ответа и разбора черновика | до конца исследования, не больше 12 месяцев; детские образцы — только по П-10, с удалением по запросу родителя | вручную, оператор |
+| Eval-наборы почерка | качество чтения ответа и разбора решения | до конца исследования, не больше 12 месяцев; детские образцы — только по П-10, с удалением по запросу родителя | вручную, оператор |
+| Образцы автора в E2: журнал проверок и выгрузки | замер E2, eval, исследование «картинка или штрихи» | решает автор: это его собственные образцы, детских среди них нет (R65) | автор: журнал — в его браузере, выгрузки — в `samples/` вне репозитория |
| Аккаунт, согласие, хеш PIN | вход, доказательство согласия, детский режим | до удаления аккаунта (П-7) | по запросу родителя |
| Журналы | безопасность, отладка | 30 дней | Cloud Logging, `us-east4` (R57) |
| История событий Centrifugo | доставка | 10 минут; мета-ключи — 1 час | TTL |
@@ -208,10 +222,10 @@
## 10. Безопасность ответов ИИ
-- **Свободного ввода текста и чата с ИИ нет.** Ребёнок пишет от руки, а модель получает картинку, поэтому до неё доходит почерк любого содержания (раздел 5.3). Диалога модель не ведёт: чтение возвращает прочитанный ответ по схеме, разбор — рамку и один вопрос по схеме. Ответ не той формы даёт просьбу переписать, и прочитанное ребёнку не показывается ([spec.md](spec.md), раздел 5).
-- **Что модели разрешено.** Системная инструкция ограничивает тему (только задача), тон (поддерживающий), длину (не больше двух предложений) и запрещает раскрывать ответ ([spec.md](spec.md), 7.2). Разбор черновика видит ключ (R39), поэтому на одну инструкцию мы не полагаемся.
+- **Свободного ввода текста и чата с ИИ нет.** Ребёнок пишет от руки, а модель получает картинку, поэтому до неё доходит почерк любого содержания (раздел 5.3). Диалога модель не ведёт: чтение возвращает прочитанный ответ по схеме, разбор — рамку и один вопрос по схеме; в E2 один вызов возвращает всё это одним ответом по схеме (R62). Ответ не той формы даёт просьбу переписать, и прочитанное ребёнку не показывается ([spec.md](spec.md), раздел 5).
+- **Что модели разрешено.** Системная инструкция ограничивает тему (только задача), тон (поддерживающий), длину (не больше двух предложений) и запрещает раскрывать ответ ([spec.md](spec.md), 7.2). Разбор видит ключ (R39), а в E2 ключ видит и чтение (R62), поэтому на одну инструкцию мы не полагаемся.
- **Что проверяет программа.** Вопрос модели проходит проверку: схема, длина, нет ссылок, нет очевидной утечки ключа, в том числе записанного словами. При провале ребёнок получает запасной текст: ловушку автора, шаблон ступени или случай попроще (R24, spec 7.3). Эта проверка — страховка от явных утечек, а не доказательство, что их нет. Перефразированную утечку ловят промпт, eval-набор и ручной разбор.
-- **Надписи-инструкции на листе.** Ребёнок может написать на листе что-то похожее на указание модели. Вердикт от этого не меняется: его выносит программа по ключу, а чтение ключа не видит (R38). Вопрос разбора проходит те же проверки, а в eval-наборе разбора есть такие надписи, написанные взрослыми.
+- **Надписи-инструкции на листе.** Ребёнок может написать на листе что-то похожее на указание модели. Вердикт от этого не меняется: его выносит программа по ключу. В двух вызовах чтение ключа не видит (R38), а в E2 программа принимает прочитанное значение, только если оно видно в записи (R62). Вопрос разбора проходит те же проверки, а в eval-наборе разбора есть такие надписи, написанные взрослыми.
- **Настройки безопасности** Gemini — по умолчанию или строже.
- **Контроль.** Следим за долей запасных подсказок и регулярно вручную разбираем выборку подсказок — без ПДн.
@@ -221,12 +235,12 @@
- Мы не знаем имени вашего ребёнка. Вы придумываете ему псевдоним и выбираете аватар.
- Ваш ребёнок не создаёт аккаунт, всё управляется из вашего.
-- Черновик и ответ, которые пишет ребёнок, хранятся только на вашем устройстве. Они стираются, когда задача решена или начата заново, через 30 дней без изменений, когда вы выходите из аккаунта или удаляете профиль.
-- Чтобы проверить ответ, мы отправляем ИИ-наставнику — Google Gemini — картинку ответа, а после неверного ответа ещё картинку черновика и саму задачу. Мы не добавляем к ним ни имени, ни других данных о ребёнке и сами эти картинки не храним. Google не обучает на них свои модели, но может какое-то время хранить запросы в журналах защиты от злоупотреблений, пока мы не оформили исключение (П-3).
+- Лист, на котором ребёнок решает и пишет ответ, хранится только на вашем устройстве. Он стирается, когда задача решена или начата заново, через 30 дней без изменений, когда вы выходите из аккаунта или удаляете профиль.
+- Чтобы проверить ответ, мы отправляем ИИ-наставнику — Google Gemini — картинку листа, а вместе с ней или после неверного ответа — саму задачу с решением. Мы не добавляем к ним ни имени, ни других данных о ребёнке и сами картинки не храним. Google не обучает на них свои модели, но может какое-то время хранить запросы в журналах защиты от злоупотреблений, пока мы не оформили исключение (П-3).
- На картинке видно всё, что ребёнок написал на листе. Попросите его не писать там своё имя и ничего личного.
- Результаты проверок — какой ответ мы прочитали, верен ли он и какие были подсказки — мы храним, чтобы показать вам прогресс. Профиль, которым год не пользовались, удаляем целиком.
- Нет рекламы, продажи данных, общения с другими пользователями и внешних ссылок.
-- Удалить профиль ребёнка или весь аккаунт можно в один шаг, после повторного входа. Из резервных копий данные уходят за 6 часов, из служебных таблиц — за сутки, из журналов — за 30 дней. Черновики на этом устройстве стираются сразу, на других ваших устройствах — когда там откроется приложение.
+- Удалить профиль ребёнка или весь аккаунт можно в один шаг, после повторного входа. Из резервных копий данные уходят за 6 часов, из служебных таблиц — за сутки, из журналов — за 30 дней. Листы на этом устройстве стираются сразу, на других ваших устройствах — когда там откроется приложение.
## 12. Открытые вопросы
@@ -240,9 +254,9 @@
| П-5 | Как подтвердить, что пользователь взрослый | отметка 18+; следствие VPC-метода | отметка 18+ при согласии и VPC-метод | С1 |
| П-6 | Сроки хранения проверок и подсказок | пока жив профиль; N месяцев | пока жив профиль; профиль — удаление после 12 месяцев неактивности без отдельного уведомления, срок указан в уведомлении при согласии. Штрихи на сервер не попадают (R42) | задание очистки, текст уведомления |
| П-7 | Хранить ли доказательство согласия после удаления аккаунта | удалять вместе с аккаунтом; хранить минимальную запись N лет | решает юрист | процедуру удаления |
-| П-8 | Может ли почерк считаться биометрией (поправки COPPA 2025 расширили понятие персональной информации) | нет — почерк не используется для идентификации; да — нужны дополнительные меры | наша оценка «нет»: почерк не используется для идентификации; картинки почерка уходят в Gemini, но не хранятся, а время и нажим штрихов не покидают устройство (R42). Подтверждает юрист | уведомление |
+| П-8 | Может ли почерк считаться биометрией (поправки COPPA 2025 расширили понятие персональной информации) | нет — почерк не используется для идентификации; да — нужны дополнительные меры | наша оценка «нет»: почерк не используется для идентификации; картинки почерка уходят в Gemini, но не хранятся, а время и нажим штрихов не покидают устройство (R42). Если исследование E2 выберет для беты штрихи вместо картинки, оценка пересматривается (R66). Подтверждает юрист | уведомление |
| П-9 | Журналы запросов Cloud Run с IP-адресами | оставить 30 дней; исключить часть журналов; сократить срок | сократить срок хранения журналов запросов до минимума, нужного для безопасности | Terraform (Logging) |
-| П-10 | Детские образцы в eval-наборах | только образцы взрослых, в том числе нарочно «детский» почерк; детские образцы с отдельным письменным согласием родителя | отдельное согласие. В нём сказано, что образцы хранятся у оператора не больше 12 месяцев, уходят в Gemini при каждом прогоне eval-набора и удаляются по запросу. Решается при разбиении эпика MVP (E2) | eval-наборы на детском почерке, текст согласия |
+| П-10 | Детские образцы в eval-наборах | только образцы взрослых, в том числе нарочно «детский» почерк; детские образцы с отдельным письменным согласием родителя | отдельное согласие. В нём сказано, что образцы хранятся у оператора не больше 12 месяцев, уходят в Gemini при каждом прогоне eval-набора и удаляются по запросу. Решается при разбиении беты (E15): в E2 eval идёт только на образцах автора (R65) | eval-наборы на детском почерке, текст согласия |
## 13. Источники
diff --git a/docs/product.md b/docs/product.md
index f1e0589..ca1538e 100644
--- a/docs/product.md
+++ b/docs/product.md
@@ -1,24 +1,24 @@
# MathTrail Canvas — продуктовый документ (MVP)
-> Статус: черновик для обсуждения, 2026-09-27. Собран из исходного черновика автора (в репозиторий не входит) и решений обсуждения 2026-09-27 ([decisions.md](decisions.md)); исправлен по итогам ревью документов. Пересмотрен 2026-10-11 при повороте к LLM-first (R37–R43).
+> Статус: черновик для обсуждения, 2026-09-27. Собран из исходного черновика автора (в репозиторий не входит) и решений обсуждения 2026-09-27 ([decisions.md](decisions.md)); исправлен по итогам ревью документов. Пересмотрен 2026-10-11 при повороте к LLM-first (R37–R43) и 2026-10-16 под эпик E2 (R60–R67).
>
-> Документ описывает, что мы строим и зачем: ценность, границы MVP, сценарии, экономику и метрики. Техническое устройство описано в [spec.md](spec.md), данные детей и законы — в [privacy.md](privacy.md). Открытые вопросы собраны в разделе 15.
+> Документ описывает, что мы строим и зачем: ценность, границы MVP, сценарии, экономику и метрики. MVP — закрытая бета, эпик E15. До неё идёт E2 — минимальный срез, который меряет, как Gemini понимает свободное решение ребёнка, сколько длится и стоит проверка (R60). Что E2 делает иначе, чем бета, сказано на месте, с пометкой «E2». Техническое устройство описано в [spec.md](spec.md), данные детей и законы — в [privacy.md](privacy.md). Открытые вопросы собраны в разделе 15.
>
> Название — «MathTrail Canvas» (О-9, R55). `canvas-api` — имя сервиса, а не продукта.
## 1. Суть продукта
-**Что это.** Интерактивный репетитор по математике для детей 1–6 классов (6–12 лет). Ребёнок решает задачу стилусом или пальцем на черновике — листе в клетку, как в тетради, — и пишет ответ в поле ответа.
+**Что это.** Интерактивный репетитор по математике для детей 1–6 классов (6–12 лет). Ребёнок решает задачу стилусом или пальцем на листе в клетку, как в тетради: рисует, считает, оставляет пометки — и там же пишет итоговый ответ. Отдельного поля ответа нет (R61).
**Главная идея.** Вместо тестов с вариантами ответа и ввода с клавиатуры ребёнок пишет от руки.
-- Ответ читает ИИ, а верен ли он, решает программа по ключу задачи (R38).
-- Если ответ неверный, наставник смотрит черновик, обводит место, где рассуждение пошло не так, и задаёт короткий наводящий вопрос — это метод Сократа (R39).
+- Итоговый ответ находит на листе и читает ИИ, а верен ли он, решает программа по ключу задачи (R37, R62).
+- Если ответ неверный, наставник смотрит решение на листе, обводит место, где рассуждение пошло не так, и задаёт короткий наводящий вопрос — это метод Сократа (R39).
- Готового ответа наставник не даёт никогда.
**Чем отличается от обычного тренажёра:**
-- смотрит ход решения в черновике и показывает, где ошибка, а не только что ответ неверный;
+- смотрит ход решения и показывает, где ошибка, а не только что ответ неверный;
- не исправляет за ребёнка, а спрашивает так, чтобы он нашёл ошибку сам;
-- не мешает, пока ребёнок пишет: наставник говорит, только когда ребёнок нажал Check;
+- не мешает, пока ребёнок пишет: наставник говорит, только когда ребёнок нажал Send Answer;
- показывает, что прочитал: если ИИ прочитал не то, ребёнок видит это сразу.
Математика — первый предмет. Проверка устроена так, что следующий предмет добавляется задачами, промптом и eval-набором, без своего движка (R37).
@@ -35,26 +35,29 @@
**Для родителя:**
- доступная замена части занятий с репетитором (в MVP бесплатно, R05);
- видно, какие задачи решены, с какой попытки и на каких ловушках ребёнок спотыкается;
-- о ребёнке хранится минимум: псевдоним, аватар и класс, без имени, фото и контактов (раздел 11). Черновик остаётся на устройстве (R42).
+- о ребёнке хранится минимум: псевдоним, аватар и класс, без имени, фото и контактов (раздел 11). Лист с решением остаётся на устройстве (R42).
**Для школ и образовательных центров — после MVP.** Автоматическая проверка решений и сводка частых ошибок группы. В MVP этого нет (R06), но архитектура это учитывает.
## 3. Аудитория и рынок
-- **Ребёнок** 6–12 лет, 1–6 класс. В детском режиме решает задачи на черновике и пишет ответ в поле ответа.
+- **Ребёнок** 6–12 лет, 1–6 класс. В детском режиме решает задачи на листе и там же пишет итоговый ответ.
- **Родитель или опекун** — владелец аккаунта. Входит через Google или Apple, даёт согласие на обработку данных ребёнка, создаёт профили детей, видит прогресс и удаляет данные. Субъект доступа в системе — всегда он (R07).
- **Рынок** — международный. Интерфейс и подсказки на английском (R02). Правовые режимы: COPPA, GDPR, UK GDPR с Age Appropriate Design Code. 152-ФЗ вне рамок ([privacy.md](privacy.md), раздел 2).
- **Устройства:**
- в первую очередь планшет со стилусом или пальцем: iPad с Apple Pencil в Safari, Android-планшеты в Chrome;
- десктоп с мышью поддерживается, но он вторичен;
- телефон — по возможности: экран мал для письма.
+
+ E2 идёт на компьютере — мышью или пером графического планшета. Планшеты, перо и ладонь — в бете (R60).
- **Запуск** — закрытая бесплатная бета по инвайт-кодам (О-11). Пока нет DPIA и договоров с обработчиками, в бете участвуют только семьи из США (П-2, П-3).
+- **До беты — E2.** Пользуется только автор, изображая ребёнка, по ключу доступа: детей, родителей и их данных в E2 нет (R60, R64).
## 4. Место в экосистеме MathTrail
- MathTrail — семейство продуктов для школьной и олимпиадной математики. Рядом уже есть mathtrail-standalone — бесплатное приложение для Claude и ChatGPT. Там модель чата пишет олимпиадные задачи с выбором ответа A–E, а сервис их проверяет.
-- MathTrail Canvas — отдельный продукт с другой механикой: рукописное решение, ответ от руки и разбор черновика. Бренд общий, аккаунты и данные — нет.
-- Задачи у Canvas свои: пять статичных задач в `tasks/` (R40). Связи с mathtrail-standalone нет. Задачи из MathTrail — в дорожной карте (раздел 14).
+- MathTrail Canvas — отдельный продукт с другой механикой: рукописное решение и ответ от руки на одном листе и разбор решения. Бренд общий, аккаунты и данные — нет.
+- Задачи у Canvas свои: в E2 — шесть статичных задач в `tasks/`, по одной на класс 1–6 (R63); задачи беты решит её разбиение. Связи с mathtrail-standalone нет. Задачи из MathTrail — в дорожной карте (раздел 14).
- MVP работает на управляемых сервисах GCP, без k8s и EDA (R01). Так быстрее выйти на рынок и дешевле держать инфраструктуру.
- Платформа MathTrail на k8s + EDA — путь развития при росте нагрузки. В неё входят identity на Ory, Kafka, Centrifugo в infra-streaming и контракты `canvas.v1` ([spec.md](spec.md), раздел 19).
@@ -63,54 +66,53 @@
### 5.1. Экран задачи
1. **Условие** — текст задачи, неизменный.
-2. **Черновик** — лист в клетку. Ребёнок пишет, рисует, стирает; ластик убирает штрих целиком. Черновик хранится на устройстве (R42).
-3. **Поле ответа (Answer)** — сюда ребёнок пишет ручкой итоговый ответ. У части задач рядом подпись единиц: «rungs», «outfits», «socks».
-4. **Слой подсказок** — прозрачный слой наставника над черновиком: рамка вокруг места ошибки и текст вопроса. Он никогда не меняет и не закрывает запись ребёнка.
+2. **Лист** в клетку. Ребёнок решает где угодно: пишет, рисует, считает, оставляет пометки, стирает — ластик убирает штрих целиком. Там же он пишет итоговый ответ. Отдельного поля ответа нет ни в E2, ни в бете (R61). Лист хранится на устройстве (R42); в E2 — только до перезагрузки страницы (R60).
+3. **Кнопка Send Answer** отправляет лист на проверку.
+4. **Слой подсказок** — прозрачный слой наставника над листом: рамка вокруг места ошибки и текст вопроса. Он никогда не меняет и не закрывает запись ребёнка.
+
+**E2:** на экране есть ещё панель автора. Она показывает разбор модели целиком, время и цену проверки, а автор размечает в ней образцы (R65). Модель видела ключ и может назвать ответ в разборе, поэтому панель — исключение из правила «ответ не показывается» на время E2: детей в E2 нет, а доступ — по ключу автора. Детскому экрану беты разбор не уходит.
### 5.2. Когда запускается проверка
-- Только по кнопке **Check**. Пока ребёнок пишет, наставник молчит: проверки по паузе нет (R38).
-- Check доступна, когда в поле ответа есть запись.
+- Только по кнопке **Send Answer**. Пока ребёнок пишет, наставник молчит: проверки по паузе нет (R61).
+- Send Answer доступна, когда на листе есть запись.
- На листе идёт одна проверка за раз.
-- По Check на сервер уходят две картинки: поле ответа и черновик. Черновик ИИ видит только после неверного ответа ([privacy.md](privacy.md), раздел 5). Сервер картинки не хранит (R42).
+- По Send Answer на сервер уходит одна картинка — PNG всего листа (R42, R61). Сервер её не хранит. Что из неё и вместе с чем видит ИИ — [privacy.md](privacy.md), раздел 5.
+- Слать ли в бете вместо картинки штрихи, решает исследование E2 (R66).
### 5.3. Педагогический цикл
-1. Ребёнок решает на черновике и пишет ответ в поле ответа.
-2. Нажимает Check. ИИ читает только картинку поля ответа — без условия и ключа (R38).
-3. Программа сравнивает прочитанное с ключом задачи:
- - **верно** — поздравление и короткое объяснение решения (5.7);
- - **неразборчиво или неоднозначно** — просьба переписать ответ. Ошибкой это не считается (5.6);
- - **неверно** — «Not yet» и то, что прочитано: «I read “6”».
-4. После неверного ответа наставник смотрит черновик. Он обводит место, где рассуждение пошло не так, и задаёт один наводящий вопрос (R39). Если места не нашлось, остаётся только вопрос.
-5. Ребёнок исправляет черновик или ответ и снова нажимает Check. Цикл повторяется.
+Цикл E2 (R62):
+
+1. Ребёнок решает на листе и там же пишет итоговый ответ. Обычно ответ написан последним, часто с «Answer:» или «=», подчёркнут или обведён — так его и ищет ИИ (R61).
+2. Нажимает Send Answer. Один вызов ИИ получает картинку листа, условие задачи, её класс, ключ и решение. Он находит итоговый ответ и разбирает решение: ход рассуждения, первую ошибку, один наводящий вопрос.
+3. Программа сверяет прочитанное с ключом задачи. ИИ возвращает ответ дважды — как написан и голым значением, — и программа принимает значение, только если оно видно в записи (R62):
+ - **верно** — «Correct!», что прочитано, и решение задачи (5.7);
+ - **неразборчиво или не по форме** — просьба переписать ответ; **ответов на листе несколько или ответа нет** — просьба обвести ответ. Ошибкой это не считается (5.6);
+ - **неверно** — «Not yet», что прочитано: «I read “6”», — рамка вокруг места ошибки и вопрос. Если места не нашлось, остаётся только вопрос.
+4. Затем — следующая задача (5.7). Лестницы подсказок в E2 нет.
```mermaid
flowchart LR
- D["Решает на черновике"] --> A["Пишет ответ в поле ответа"]
- A --> C{"Check"}
- C -->|"лимит исчерпан"| X["«I've helped all I can for now»,
без ИИ"]
- C -->|"лимит есть"| R["ИИ читает ответ"]
+ D["Решает на листе
и пишет итоговый ответ"] --> C{"Send Answer"}
+ C -->|"потолок исчерпан"| X["«I've helped all I can for now»,
без ИИ"]
+ C -->|"потолок не исчерпан"| R["ИИ находит ответ
и разбирает решение"]
R -->|"сбой"| N["«Couldn't reach the checker»"]
R -->|"неразборчиво"| U["Просьба переписать ответ"]
+ R -->|"ответов несколько или нет"| O["«Circle your answer»"]
R -->|"прочитано"| K{"Программа сверяет с ключом"}
- K -->|"верно"| OK["Поздравление и объяснение"]
- K -->|"неверно"| V["«Not yet», и что прочитано"]
- V --> L{"Неверных ответов не больше трёх?"}
- L -->|"да"| E{"Черновик пуст?"}
- E -->|"нет"| Z["ИИ смотрит черновик"]
- E -->|"да"| T["Вопрос автора задачи или шаблон"]
- Z -->|"разобрал"| M["Рамка, если место нашлось,
и вопрос"]
- Z -->|"сбой"| T
- L -->|"нет"| H["Позвать взрослого или начать заново"]
- M --> D
- T --> D
- H --> D
- U --> A
- N --> A
+ K -->|"верно"| OK["«Correct!» и решение"]
+ K -->|"неверно"| V["«Not yet», что прочитано,
рамка и вопрос"]
+ OK --> NX["Next task"]
+ V --> NX
+ U --> D
+ O --> D
+ N --> D
X --> D
```
+**Бета (E15)** — тот же лист и та же кнопка. Один вызов или два, как в R38 и R39, решают выводы E2 (T76). Лестница подсказок (5.4) и «решить заново» возвращаются в бете.
+
### 5.4. Правила подсказки
- никогда не называть ответ;
@@ -119,7 +121,9 @@ flowchart LR
- поддерживающий тон: без «неправильно» в лоб; начинать с того, что уже получилось;
- говорить только о задаче: никаких посторонних тем, ссылок и личных вопросов.
-**Лестница** — по числу неверных ответов на этом листе (R39, закрывает О-12):
+**E2:** после неверного ответа — вопрос модели, прошедший проверку утечки. Не прошёл — текст ловушки автора задачи или шаблон (R24). Ступеней нет (R39).
+
+**Лестница — в бете (E15)**, по числу неверных ответов на этом листе (R39, закрывает О-12):
1. рамка и общий вопрос о месте ошибки;
2. вопрос конкретнее — о правиле или ловушке;
3. похожий случай попроще — например, лестница покороче, — без решения самой задачи.
@@ -130,32 +134,39 @@ flowchart LR
| Ситуация | Что на экране |
|---|---|
-| Ответ верный | «Correct! I read “7”.», короткое объяснение решения и поздравление |
-| Ответ неверный | сразу «Not yet. I read “6”.», следом рамка на черновике и вопрос; если места не нашлось — только вопрос |
+| Ответ верный | «Correct! I read “7”.», решение задачи и поздравление |
+| Ответ неверный | «Not yet. I read “6”.», рамка на листе и вопрос; если места не нашлось — только вопрос. Ответ задачи не показывается |
| Не прочитать | «I couldn't read your answer. Write it a little bigger, on one line.» или подсказка формы: «Write just the number.» |
+| На листе несколько ответов или ни одного | «Circle your answer.» |
| Проверка недоступна | «Couldn't reach the checker. Your answer is kept. Send it again in a moment.» |
-| Дневной лимит исчерпан | «I've helped all I can for now. Keep going on your own!» (R35) |
-| После третьей ступени | предложение позвать взрослого или начать заново: кнопки «Ask a grown-up» и «Start again» |
+| Дневной лимит исчерпан | «I've helped all I can for now. Keep going on your own!» (R35; в E2 — дневной потолок, R64) |
+| В бете, после третьей ступени | предложение позвать взрослого или начать заново: кнопки «Ask a grown-up» и «Start again» |
+
+После неразборчивого ответа и сбоя ребёнок исправляет лист и снова нажимает Send Answer. В E2 после вердикта — кнопка Next task (5.7).
-Рамка ложится на штрихи самого ребёнка, а не на догадку модели. Если в месте, которое назвала модель, штрихов нет, рамки нет (R43).
+В E2 вердикт, рамка и вопрос приходят одним ответом сервера (R23). Рамка ложится на штрихи самого ребёнка, а не на догадку модели. Если в месте, которое назвала модель, штрихов нет, рамки нет (R43).
### 5.6. Неразборчивый почерк и сбои
-- **Ответ прочитан неуверенно, неоднозначно или не по форме** (два числа, «maybe»). Ребёнок видит просьбу переписать. Ошибкой это не считается, и лестница не растёт.
+- **Ответ прочитан неуверенно, неоднозначно или не по форме** (два числа в одной записи, «maybe»). Ребёнок видит просьбу переписать. Ошибкой это не считается, и лестница беты не растёт.
+- **На листе несколько ответов с разными значениями или ни одного.** Ребёнок видит просьбу обвести свой ответ: «Circle your answer.» Это тоже не ошибка (R61, R62).
- **ИИ недоступен или не ответил вовремя.** Ребёнок видит нейтральное «Couldn't reach the checker…», а не ошибку. Вызов, который успел уйти к модели, засчитывается в дневной лимит, даже если не удался: он может быть оплачен (R35). Если до вызова дело не дошло, лимит не тратится.
-- **Исчерпан дневной лимит** (раздел 9). Ребёнок решает дальше сам: запись сохраняется, но пока лимит не обновится, наставник молчит — ни рамок, ни вопросов, ни поздравления. На каждое нажатие Check он говорит: «I've helped all I can for now. Keep going on your own!» (R35).
-- **Ответ неверный, но разобрать черновик не вышло**: сбой, пустой лист или вопрос модели не прошёл проверку. Ребёнок получает заготовленный вопрос автора задачи или шаблон (R24).
-- **Верность ответа решает ключ задачи, а не ИИ**: модель не может назвать верный ответ неверным (R38). Ошибиться может только чтение. Поэтому ребёнок видит, что прочитано, а при сомнении сервис просит переписать. **Ложная ошибка хуже, чем отсутствие проверки.**
-- **Ребёнок нажал Check снова или стёр обведённое.** Рамка и вопрос исчезают.
+- **Исчерпан дневной лимит** (раздел 9). Ребёнок решает дальше сам: запись сохраняется, но пока лимит не обновится, наставник молчит — ни рамок, ни вопросов, ни поздравления. На каждое нажатие Send Answer он говорит: «I've helped all I can for now. Keep going on your own!» (R35). В E2 лимит — дневной потолок вызовов ИИ на весь сервис, а не на ребёнка (R64).
+- **Ответ неверный, но вопроса модели нет**: она его не дала, или он не прошёл проверку утечки. Ребёнок получает текст ловушки автора задачи или шаблон (R24). В бете с двумя вызовами сюда же попадают сбой разбора и пустой лист.
+- **Верность ответа решает ключ задачи, а не ИИ**: модель не может назвать верный ответ неверным, и её собственная оценка ответа вердикт не решает (R37, R62). Ошибиться может чтение — и выбор итога: ИИ может принять за ответ промежуточное число или пометку (R61). Поэтому ребёнок видит, что прочитано, а при сомнении сервис просит переписать или обвести ответ. **Ложная ошибка хуже, чем отсутствие проверки.**
+- **Ребёнок стёр обведённое** — рамка и вопрос исчезают. В бете они исчезают и при новом нажатии Send Answer; в E2 после вердикта следующая проверка идёт уже на новой задаче (5.7).
### 5.7. Завершение задачи
- Задача решена, когда ответ совпал с ключом.
-- Ребёнок видит «Correct! I read “7”.», короткое объяснение решения и анимацию. Объяснение приходит только после верного ответа.
-- Затем предлагается следующая задача или «решить заново» с чистым листом (О-16).
+- Ребёнок видит «Correct! I read “7”.», решение задачи и анимацию. Решение приходит только после верного ответа.
+- **E2:** после вердикта — Next task: чистый лист и следующая задача. После шестой — снова первая; автор может и сам выбрать задачу 1–6 (R63).
+- **Бета:** следующая задача или «решить заново» с чистым листом (О-16).
## 6. Сценарии MVP
+Сценарии беты (E15). В E2 нет ни родителей, ни детей: автор сам решает задачи по циклу раздела 5.3 (R60).
+
**С1. Первый вход.**
1. Родитель открывает приложение и входит через Google или Apple.
2. Вводит инвайт-код беты (О-11).
@@ -163,61 +174,75 @@ flowchart LR
4. После подтверждения создаёт профиль ребёнка: псевдоним (форма подсказывает не вводить настоящее имя), аватар из набора, класс.
5. Задаёт PIN детского режима.
-**С2. Детский режим.** Родитель выбирает профиль и передаёт устройство ребёнку. Ребёнок видит только задачи, черновик и поле ответа. Выход в родительский раздел закрыт PIN.
+**С2. Детский режим.** Родитель выбирает профиль и передаёт устройство ребёнку. Ребёнок видит только задачи, лист и кнопку Send Answer. Выход в родительский раздел закрыт PIN.
**С3. Верно с первой попытки.**
-1. Ребёнок открывает задачу про лестницу. На черновике рисует перекладины и считает промежутки.
-2. Пишет 7 в поле ответа и нажимает Check.
-3. Видит «Correct! I read “7”.», объяснение и поздравление, затем — следующую задачу.
+1. Ребёнок открывает задачу про лестницу. На листе рисует перекладины и считает промежутки.
+2. Под рисунком пишет «Answer: 7» и нажимает Send Answer.
+3. Видит «Correct! I read “7”.», решение и поздравление, затем — следующую задачу.
**С4. Ошибка и лестница.** Задача про носки.
-1. Ребёнок пишет 2 и нажимает Check. Сразу видит «Not yet. I read “2”.», следом — рамку вокруг «1 red + 1 blue» на черновике и вопрос.
-2. Пишет 6 — вопрос становится конкретнее.
+1. Ребёнок пишет под решением 2 и нажимает Send Answer. Видит «Not yet. I read “2”.», рамку вокруг «1 red + 1 blue» на листе и вопрос.
+2. Зачёркивает 2, пишет 6 — вопрос становится конкретнее.
3. Снова неверно — похожий случай попроще.
4. Ещё раз неверно — наставник предлагает позвать взрослого или начать заново.
5. Ребёнок пишет 3 — «Correct!».
**С5. Неразборчиво или сбой.** Поведение описано в разделе 5.6.
-**С6. Прогресс для родителя.** Минимум (О-14): какие задачи начаты и решены, с какой попытки, сколько было подсказок, на каких ловушках ребёнок спотыкался, когда было последнее занятие. Записи решения нет: черновик живёт на устройстве (R42).
+**С6. Прогресс для родителя.** Минимум (О-14): какие задачи начаты и решены, с какой попытки, сколько было подсказок, на каких ловушках ребёнок спотыкался, когда было последнее занятие. Записи решения нет: лист живёт на устройстве (R42).
**С7. Удаление.** Родитель удаляет профиль ребёнка со всем, что с ним связано, или весь аккаунт.
- Подтверждение — повторный вход: PIN для этого недостаточно, ведь ребёнок может его подсмотреть.
-- Черновики на этом устройстве стираются сразу, а на другом устройстве родителя — когда там откроется приложение (R42).
+- Листы на этом устройстве стираются сразу, а на другом устройстве родителя — когда там откроется приложение (R42).
- Сроки полного удаления, включая резервные копии, — в [privacy.md](privacy.md), раздел 7.
**С8. Доступ в бету.** Войти может только родитель с одноразовым инвайт-кодом (О-11). Код не привязан к email, поэтому работает и с Apple «Hide My Email». Без кода — лист ожидания.
## 7. Объём MVP
-**Входит:**
+MVP — закрытая бета, эпик E15. До неё идёт E2 (R60); его объём — в конце раздела.
+
+**Входит в бету:**
- веб-приложение для планшетов и десктопа с интерфейсом на английском;
- вход родителя через Google или Apple, инвайт-код, подтверждённое оператором согласие, несколько профилей детей (лимит — О-13);
- детский режим с PIN;
-- пять статичных задач (R40);
-- проверка ответа: ИИ читает ответ, программа сравнивает его с ключом (R38);
-- разбор черновика после неверного ответа: рамка и наводящий вопрос (R39);
+- статичные задачи в `tasks/` — какие, решит разбиение E15 (R63);
+- лист без поля ответа и кнопка Send Answer (R61);
+- проверка ответа: ИИ находит итоговый ответ на листе и читает его, программа сравнивает его с ключом (R37). Один вызов или два — по выводам E2 (T76);
+- разбор решения после неверного ответа: рамка и наводящий вопрос (R39);
- лестница подсказок;
- доставка подсказок в реальном времени;
+- лист на устройстве между перезагрузками страницы (R42);
- минимальный экран прогресса для родителя;
- удаление профиля ребёнка и аккаунта;
- дневные лимиты на ребёнка, закрытая бета по приглашениям.
-**Не входит:**
+**Не входит в бету:**
- оплата и подписка (R05);
- учителя, классы, школы, групповая аналитика (R06);
-- каталог задач, их подбор, адаптивная сложность (R40);
+- каталог задач, их подбор, адаптивная сложность (R63);
- собственный вход ребёнка со своего устройства (R07);
- нативные приложения для iOS и Android;
- голос и видео, в том числе Gemini Live;
- языки, кроме английского;
- офлайн-режим;
- 7 класс и старше;
-- чат с ИИ и ввод текста с клавиатуры: ребёнок пишет только рукой, а ответ — коротко, в поле ответа;
+- чат с ИИ и ввод текста с клавиатуры: ребёнок пишет только рукой, а итоговый ответ — коротко, на листе;
- проверка каждого шага по мере письма (R37);
-- хранение черновика на сервере и запись решения для родителя (R42);
+- хранение листа на сервере и запись решения для родителя (R42);
- задачи со стрелками — прежний путь Б (R37);
-- фото листа с домашним заданием, другие предметы и задачи из MathTrail — в дорожной карте (раздел 14; R37, R40).
+- фото листа с домашним заданием, другие предметы и задачи из MathTrail — в дорожной карте (раздел 14; R37, R63).
+
+**Объём E2** — минимальный срез с замером ИИ (R60):
+- шесть статичных задач, по одной на класс 1–6, разных видов ответа и способов решения (R63);
+- лист без поля ответа и Send Answer; один вызов ИИ разбирает лист, программа выносит вердикт; затем — Next task (раздел 5.3; R61, R62);
+- доступ по ключу автора и дневной потолок вызовов ИИ (R64);
+- панель автора, его разметка, журнал проверок на его устройстве с выгрузкой и прогон журнала на других моделях и настройках — `just eval` (R65);
+- исследование: слать модели картинку листа или штрихи (R66);
+- компьютер с мышью или графическим планшетом (R60).
+
+Остальное из списка беты — вход, согласие, PIN, профили детей, лимиты на ребёнка, лестница и «решить заново», realtime, лист между перезагрузками, прогресс, удаление — в E2 не входит. Не входят и телеметрия, нагрузочные и e2e-тесты, планшеты. Результат E2 — замеры и выводы для беты: насколько хорошо модель понимает решение и проверяет ответ, сколько длится и стоит проверка (RUN.md, таблица 1).
## 8. UX-принципы для детей
@@ -226,14 +251,16 @@ flowchart LR
- **Коротко.** Подсказка — не больше двух предложений. Для младших задач слова проще.
- **Удобный ввод.** Крупные кнопки и области касания. Работа пальцем и стилусом; стилус в приоритете, касание ладонью игнорируется.
- **Запись ребёнка неприкосновенна.** Наставник только обводит место поверх неё.
-- **Наставник говорит только по Check.** Пока ребёнок пишет, реакции нет.
+- **Наставник говорит только по Send Answer.** Пока ребёнок пишет, реакции нет.
- **Показываем, что прочитали.** «I read “6”» сразу показывает ребёнку, если ИИ прочитал не то.
- **Ничего лишнего.** Нет внешних ссылок, рекламы и «тёмных паттернов» (UK AADC, [privacy.md](privacy.md)).
## 9. Юнит-экономика
+Расчёт ниже сделан 2026-10-11 для беты с двумя вызовами — чтением ответа (R38) и разбором черновика (R39). В E2 вызов один, и он видит ключ (R62); его цену и задержку меряют журнал автора и eval на Flash-Lite, Flash и Pro (R65). Сколько вызовов будет в бете, какие модели и настройки — а с ними и этот расчёт — решают выводы E2 (T76). Бюджет проекта на время E2 — $20 в месяц (R60).
+
**Из чего складывается стоимость:**
-- **чтение ответа** — Gemini класса Flash-Lite или Flash (R22), на каждое нажатие Check (R38);
+- **чтение ответа** — Gemini класса Flash-Lite или Flash (R22), на каждое нажатие Send Answer (R38);
- **разбор черновика и вопрос** — Gemini Flash, только после неверного ответа и не больше трёх раз на лист (R39);
- **инфраструктура** — Cloud Run, Neon, Upstash, Firebase; в бете — в основном в пределах бесплатных лимитов.
@@ -242,7 +269,7 @@ flowchart LR
N_разборов ≤ 3 на лист
```
-**Цена вызовов.** Цены Gemini сверены 2026-10-11, число токенов — оценка до спайков S1 и S2. У моделей Gemini 3 потолок токенов на картинку задаёт параметр `media_resolution`: по умолчанию 1 120, на уровне `low` — 280. Оценка берёт потолок по умолчанию.
+**Цена вызовов.** Цены Gemini сверены 2026-10-11, число токенов — оценка до замеров E2. У моделей Gemini 3 потолок токенов на картинку задаёт параметр `media_resolution`: по умолчанию 1 120, на уровне `low` — 280. Оценка берёт потолок по умолчанию.
- **Чтение:** Gemini 3.5 Flash-Lite, $0.30 за 1 млн входных токенов и $2.50 за 1 млн выходных. Около 1 400 входных токенов (картинка — 1 120) и 200 выходных с учётом размышлений — ≈ $0.0009.
- **Разбор:** Gemini 3.8 Flash, $0.75 и $3.75 до 31.12.2026, с 01.01.2027 — $1.50 и $7.50. Около 2 000 входных токенов (картинка — 1 120) и 700 выходных — ≈ $0.004, с 2027 года ≈ $0.008.
@@ -258,9 +285,9 @@ N_разборов ≤ 3 на лист
**Выводы:**
- **Главная статья — разбор черновика:** в задаче с ошибкой на него приходится 70–80% стоимости, а с 2027 года — больше 80%. Рычаги:
- разбор только после неверного ответа и не больше трёх на лист;
- - уровень `media_resolution` для черновика — его подбирает спайк S2;
- - Flash-Lite вместо Flash для разбора — только если спайк S2 покажет, что его хватает; это пересмотр R22.
-- **Чтение дешёвое, но идёт на каждое нажатие Check.** Уровень `low` снизит его до ≈ $0.0007, если точности хватит (спайк S1). Если S1 выберет для чтения Flash, чтение подорожает вдвое, а с 2027 года — вчетверо, и средняя задача в 2027 году превысит цель раздела 12 — $0.01.
+ - уровень `media_resolution` для картинки листа — его подбирает eval E2;
+ - Flash-Lite вместо Flash для разбора — только если eval E2 покажет, что его хватает; это пересмотр R22.
+- **Чтение дешёвое, но идёт на каждое нажатие Send Answer.** Уровень `low` снизит его до ≈ $0.0007, если точности хватит (eval E2). Если eval выберет для чтения Flash, чтение подорожает вдвое, а с 2027 года — вчетверо, и средняя задача в 2027 году превысит цель раздела 12 — $0.01.
- **Разбор подорожает с 2027 года:** цены Gemini 3.8 Flash удваиваются.
- **Инфраструктура в бете — в основном в бесплатных лимитах. Первым их исчерпает Cloud Run.**
- Бесплатные лимиты: Cloud Run — 2 млн запросов, 180 000 vCPU-секунд и 360 000 ГиБ-секунд в месяц; Neon — 0,5 ГБ и 100 CU-часов; Upstash — 500 тыс. команд; Firebase Auth — 50 000 активных пользователей в месяц.
@@ -280,24 +307,24 @@ N_разборов ≤ 3 на лист
| Требование | Цель MVP | Как проверяем |
|---|---|---|
-| Вердикт после Check: «Correct!», «Not yet» или просьба переписать | ≤ 2 с (p95) | спайк S4, метрики |
-| Рамка и вопрос после неверного ответа | ≤ 6 с от нажатия Check (p95) | спайк S4, метрики |
+| Вердикт после Send Answer: «Correct!», «Not yet» или просьба переписать | ≤ 2 с (p95) | спайк S4, метрики |
+| Рамка и вопрос после неверного ответа | ≤ 6 с от нажатия Send Answer (p95) | спайк S4, метрики |
| Первый запрос после простоя (холодный старт) | ≤ 3 с на запуск сервиса, без учёта вызовов модели | спайк S4 |
-| Сохранность записи | черновик и поле ответа хранятся на устройстве и переживают перезагрузку страницы и обрыв сети. Повтор той же проверки получает сохранённый ответ без нового вызова ИИ (R42) | e2e-тест с обрывом сети и перезагрузкой |
+| Сохранность записи | лист хранится на устройстве и переживает перезагрузку страницы и обрыв сети. Повтор той же проверки получает сохранённый ответ без нового вызова ИИ (R42) | e2e-тест с обрывом сети и перезагрузкой |
| Картинки проверки | сервис их не сохраняет: ни в базе, ни в журналах, ни в трассах (R42) | тест схемы данных, аудит журналов |
| Доступность сервиса | лучшее возможное в бете | мониторинг |
| Устройства | iPad (Safari, Apple Pencil), Android-планшет (Chrome), десктоп (Chrome, Safari, Firefox, Edge), последние версии | ручная проверка |
| Язык интерфейса | английский | — |
| Доступность для всех | контраст, крупные элементы; озвучка — после MVP | ручная проверка |
-Все цели по задержке — оценки. Их подтверждает спайк S4.
+Все цели по задержке — оценки. Их подтверждает спайк S4. Это цели беты: в E2 нет ни лестницы, ни второго вызова, ни хранения листа между перезагрузками, а задержку его одного вызова меряют журнал автора и eval (R65). Цели беты по задержке пересматривают выводы E2 (T76).
## 11. Приватность и безопасность ребёнка: что обещаем родителям
- О ребёнке храним только псевдоним, аватар из набора и класс. Имени, фото, даты рождения и контактов нет.
- Аккаунт и согласие принадлежат родителю. Ребёнок не регистрируется.
-- Черновик и ответ ребёнка хранятся только на устройстве, где он решает. На сервере остаются результаты проверок: прочитанный ответ, если это число или вариант из списка, верен ли он, на какой ловушке задачи ребёнок споткнулся и подсказки наставника. Они нужны для работы сервиса и показа прогресса ([privacy.md](privacy.md), раздел 8).
-- Для проверки картинка ответа уходит в Google Gemini — модель ИИ, которая её читает. После неверного ответа туда же уходят картинка черновика и задача с решением. Сервис не добавляет к картинкам ни имени, ни псевдонима, ни идентификаторов. Картинки мы не храним, а поставщик не обучает на них свои модели ([privacy.md](privacy.md), раздел 5).
+- Лист ребёнка — решение и ответ — хранится только на устройстве, где он решает. На сервере остаются результаты проверок: прочитанный ответ, если это число или вариант из списка, верен ли он, на какой ловушке задачи ребёнок споткнулся и подсказки наставника. Они нужны для работы сервиса и показа прогресса ([privacy.md](privacy.md), раздел 8).
+- Для проверки картинка листа уходит в Google Gemini — модель ИИ, которая её читает, — вместе с задачей и её решением. Что именно и в скольких вызовах, сказано в [privacy.md](privacy.md), раздел 5.1: для беты это решают выводы E2. Сервис не добавляет к картинкам ни имени, ни псевдонима, ни идентификаторов. Картинки мы не храним, а поставщик не обучает на них свои модели ([privacy.md](privacy.md), раздел 5).
- Профиль или аккаунт удаляется в один шаг.
- Нет рекламы, продажи данных, чатов и внешних ссылок.
- ИИ говорит только о задаче, поддерживающим тоном, и не выдаёт ответ.
@@ -316,8 +343,9 @@ N_разборов ≤ 3 на лист
| Ложно верные | доля неверных ответов, получивших «Correct!» | ≤ 0,5% |
| Лишние просьбы переписать | доля разборчивых ответов, на которые ребёнок получил просьбу переписать | ≤ 5% |
| Точность чтения | доля ответов, прочитанных в точности как написано | наблюдаем |
+| Ответ найден | доля листов, где за итоговый ответ принят именно он, а не промежуточное число или пометка (R61) | наблюдаем |
| Точность места | доля разборов, где рамка стоит на месте ошибки; доля рамок не на месте ошибки | ≥ 80%; ≤ 10% |
-| Качество подсказок | доля подсказок, прошедших рубрику: не раскрывает ответ, один вопрос, по возрасту, по делу, не противоречит черновику | ≥ 95% на ручной выборке; ни одной утечки ответа |
+| Качество подсказок | доля подсказок, прошедших рубрику: не раскрывает ответ, один вопрос, по возрасту, по делу, не противоречит решению на листе | ≥ 95% на ручной выборке; ни одной утечки ответа |
| Запасные подсказки | доля подсказок из текста автора задачи или шаблона вместо вопроса модели | наблюдаем |
| Задержка | p95 вердикта и p95 вопроса с рамкой | ≤ 2 с и ≤ 6 с |
| Доведение до конца | доля начатых задач, решённых полностью | наблюдаем |
@@ -325,42 +353,45 @@ N_разборов ≤ 3 на лист
| Стоимость | средняя стоимость решённой задачи | ≤ $0.01 |
Откуда берутся данные:
-- eval-наборы чтения ответов и разбора черновиков — офлайн;
+- eval-наборы чтения ответов и разбора решений — офлайн;
- записи проверок: вердикт, прочитанный ответ, если это число или вариант из списка, и совпавшая ловушка; метрики по ним считаются без идентификаторов детей;
- ручной разбор выборки подсказок;
- сигналы о неверном чтении (О-17).
-**Критерии приёмки MVP:**
-1. Все пять задач решаются от начала до конца на iPad в Safari с Apple Pencil, на Android-планшете в Chrome и на десктопе.
+**E2.** Критериев приёмки беты у E2 нет: его результат — замеры и выводы (RUN.md, таблица 1). Те же метрики качества — ложные тревоги, ложно верные, лишние просьбы, точность чтения и места, найден ли ответ, утечки — E2 меряет на журнале автора и в eval: на образцах автора, размеченных им при письме, с ключом в промпте и без (R62, R65). Ещё — задержку клиента, сервера и модели, токены и цену каждой проверки.
+
+**Критерии приёмки беты (E15):**
+1. Все задачи беты решаются от начала до конца на iPad в Safari с Apple Pencil, на Android-планшете в Chrome и на десктопе.
2. На eval-наборе чтения выполнены цели по ложным тревогам и ложно верным ответам. Неразборчивый ответ получает просьбу переписать, а не «Not yet».
3. На eval-наборе разбора ни одна подсказка не раскрывает ответ, а точность места не ниже цели.
-4. Когда чтение ответа не удалось или дневной лимит исчерпан, ребёнок видит нейтральную фразу или фразу лимита, а не ошибку. Когда не удался разбор черновика, ребёнок получает вопрос автора задачи или шаблон.
-5. Удаление профиля и аккаунта стирает данные во всех хранилищах; черновики на устройствах — как в сценарии С7 ([privacy.md](privacy.md), раздел 7).
+4. Когда чтение ответа не удалось или дневной лимит исчерпан, ребёнок видит нейтральную фразу или фразу лимита, а не ошибку. Когда не удался разбор решения, ребёнок получает вопрос автора задачи или шаблон.
+5. Удаление профиля и аккаунта стирает данные во всех хранилищах; листы на устройствах — как в сценарии С7 ([privacy.md](privacy.md), раздел 7).
6. Лимиты беты срабатывают, бюджетные оповещения настроены.
7. В журналах приложения и метриках нет персональных данных. Срок хранения журналов запросов платформы, где есть IP-адреса, задан по П-9.
-8. Сервис нигде не сохраняет картинки ответа и черновика: ни в базе, ни в журналах, ни в трассах (R42).
+8. Сервис нигде не сохраняет картинку листа: ни в базе, ни в журналах, ни в трассах (R42).
## 13. Риски и гипотезы
| Риск или гипотеза | Почему важно | Что делаем |
|---|---|---|
-| ИИ неверно читает детский почерк | верный ответ получит «Not yet» — ложная тревога | ребёнок видит, что прочитано; при сомнении — просьба переписать (R38); спайк S1 на образцах взрослых, в том числе с нарочно «детским» почерком; сигналы о неверном чтении (О-17) |
-| Разбор черновика видит ключ | вопрос может раскрыть ответ и сломать педагогику | каждый вопрос проходит проверку утечки ключа; запасной текст автора; eval-набор и рубрика (R24, R39) |
-| Рамка не на месте ошибки | ребёнок ищет ошибку там, где её нет | рамка ложится только на штрихи ребёнка (R43); метрика точности места; спайк S2 |
-| Вопрос противоречит черновику | наставник выглядит нелепо, ребёнок перестаёт ему верить | рубрика «не противоречит черновику» на eval-наборе; спайк S2 |
+| ИИ неверно читает детский почерк | верный ответ получит «Not yet» — ложная тревога | ребёнок видит, что прочитано; при сомнении — просьба переписать (R38); eval E2 на образцах взрослых, в том числе с нарочно «детским» почерком; сигналы о неверном чтении (О-17) |
+| ИИ принимает за ответ не то число | промежуточное число, пометка или ключ, записанный по ходу решения, дают ложный вердикт | просьба обвести ответ, если ответов несколько; метрика «ответ найден»; в живом прогоне E2 ответ нарочно пишется не последним, а ключ — промежуточным числом (R61) |
+| Разбор решения видит ключ | вопрос может раскрыть ответ и сломать педагогику | каждый вопрос проходит проверку утечки ключа; запасной текст автора; eval-набор и рубрика (R24, R39) |
+| Рамка не на месте ошибки | ребёнок ищет ошибку там, где её нет | рамка ложится только на штрихи ребёнка (R43); метрика точности места; eval E2 |
+| Вопрос противоречит решению на листе | наставник выглядит нелепо, ребёнок перестаёт ему верить | рубрика «не противоречит решению на листе» на eval-наборе; eval E2 |
| Ребёнок подбирает ответ перебором | задача «решена» без решения | родитель видит, с какой попытки решена задача (О-14); после трёх ступеней лестницы — предложение позвать взрослого; дневной лимит проверок |
| На листе может быть что угодно, и это может уйти в ИИ картинкой | ребёнок может написать своё имя | уведомление для родителей; без идентификаторов; картинки не храним ([privacy.md](privacy.md), раздел 5.3) |
-| Надпись на листе как инструкция модели | модель уходит от задачи или «соглашается», что ответ верен | верность решает ключ, а не модель (R38); чтение не видит ключа; ответ модели ограничен схемой, вопрос проходит проверки (R24); такие случаи — в eval-наборах |
-| Один поставщик на оба вызова | сбой Gemini — нет проверки | порты; при сбое — нейтральная фраза или запасной вопрос, а не ошибка (раздел 5.6) |
-| Gemini Flash дорожает с 2027 года | экономика | лимиты; пересмотр модели разбора по итогам спайка S2 (R22) |
-| Черновик на устройстве теряется | браузер может стереть данные сайта, и ребёнок потеряет запись | принятая цена (R42); SPA просит браузер хранить данные сайта постоянно |
+| Надпись на листе как инструкция модели | модель уходит от задачи или «соглашается», что ответ верен | верность решает ключ, а не модель (R38); в двух вызовах чтение не видит ключа, а в одном вызове E2 программа принимает прочитанное значение, только если оно видно в записи (R62); ответ модели ограничен схемой, вопрос проходит проверки (R24); такие случаи — в eval-наборах |
+| Один поставщик на все вызовы | сбой Gemini — нет проверки | порты; при сбое — нейтральная фраза или запасной вопрос, а не ошибка (раздел 5.6) |
+| Gemini Flash дорожает с 2027 года | экономика | лимиты; пересмотр модели разбора по итогам eval E2 (R22) |
+| Лист на устройстве теряется | браузер может стереть данные сайта, и ребёнок потеряет запись | принятая цена (R42); SPA просит браузер хранить данные сайта постоянно |
| Стоимость WebSocket | экономика продукта | лимиты, замеры; спайк S3 |
| Жизненный цикл моделей Gemini | модели снимают примерно через год | закреплённые версии; eval-наборы при смене (R22) |
| Проверяемое согласие родителя по COPPA | обязательный юридический шаг | П-1; перед публичным запуском — юрист |
## 14. Дорожная карта после MVP
-Каждый пункт — эпик таблицы 2 [RUN.md](../RUN.md) (R44), его номер — в скобках. Там же порядок, в котором эпики берут в работу. Технические эпики — вторая среда (E3) и периметр с подписью образа (E13) — тоже там.
+Каждый пункт — эпик таблицы 2 [RUN.md](../RUN.md) (R44), его номер — в скобках. Там же порядок, в котором эпики берут в работу. Первым в ней стоит сам MVP — закрытая бета (E15): её строят после E2 по его выводам (R60). Технические эпики — вторая среда (E3) и периметр с подписью образа (E13) — тоже там.
1. Каталог задач и подбор под ребёнка; новые темы: дроби, порядок действий, текстовые задачи (E4).
2. Вход ребёнка со своего устройства по коду или QR от родителя (E5; R07).
@@ -386,18 +417,18 @@ N_разборов ≤ 3 на лист
| № | Вопрос | Решение | Дата |
|---|---|---|---|
-| О-1 | Типы задач в MVP | Задачи по математике с коротким ответом — числом или вариантом из списка. Проверяется итоговый ответ по ключу, а после неверного ответа — ещё и черновик (R37, R38, R39). Прежнее решение R03 заменено | 2026-10-11 |
+| О-1 | Типы задач в MVP | Задачи по математике с коротким ответом — числом или вариантом из списка. Проверяется итоговый ответ по ключу, а после неверного ответа — ещё и решение на листе (R37, R38, R39). Прежнее решение R03 заменено | 2026-10-11 |
| О-2 | Состав стека | Как в черновике (R08), без Mathpix; OpenFGA — сайдкар (R09). Убирать Centrifugo и OpenFGA автор не стал (R41) | 2026-10-11 |
| О-3 | Рынок и язык интерфейса | Международный, сначала английский (R02) | 2026-09-27 |
| О-4 | Оплата в MVP | Нет, закрытая бесплатная бета (R05) | 2026-09-27 |
| О-5 | Как ребёнок попадает на холст | Устройство родителя, детский режим с PIN (R07) | 2026-09-27 |
| О-6 | Учителя в MVP | Нет, только семья (R06) | 2026-09-27 |
-| О-7 | Откуда задачи | Пять статичных задач из макета в `tasks/` — временно, до каталога (R40). Прежнее решение R04 заменено | 2026-10-11 |
+| О-7 | Откуда задачи | Статичные задачи в `tasks/` — временно, до каталога. В E2 их шесть, их пишет Claude, автор вычитывает (R63). Прежние решения R04 и R40 заменены | 2026-10-15 |
| О-8 | Где и на каком языке документы | `docs/`, русский (R25) | 2026-09-27 |
| О-9 | Название продукта | «MathTrail Canvas»; адреса — `run.app` у API и `web.app` у SPA, своего домена в бете нет (R55) | 2026-10-14 |
-| О-10 | Какие именно задачи | Пять из шести задач макета, без задачи с чашками (R40) | 2026-10-11 |
+| О-10 | Какие именно задачи | В E2 — шесть задач, по одной на класс 1–6, разных видов ответа и способов решения (R63). Задачи беты решит разбиение E15. Прежнее решение — пять задач макета (R40) — заменено | 2026-10-15 |
| О-11 | Как попасть в бету | Одноразовые инвайт-коды, не allowlist email (R29): список email ломается на Apple «Hide My Email», а хеши email в конфигурации раскрывают участников | 2026-09-27 |
-| О-12 | Лестница подсказок | Три ступени по числу неверных ответов, затем предложение позвать взрослого или начать заново; ответ не показываем никогда (R39) | 2026-10-11 |
+| О-12 | Лестница подсказок | Три ступени по числу неверных ответов, затем предложение позвать взрослого или начать заново; ответ не показываем никогда (R39). В E2 лестницы нет — она в бете (R62) | 2026-10-15 |
| О-15 | Отмечать ли верный шаг | Вопрос снят: шаги не проверяются, а верный ответ получает «Correct!» (R37, R38) | 2026-10-11 |
### 15.2. Открыто
@@ -405,8 +436,8 @@ N_разборов ≤ 3 на лист
| № | Вопрос | Варианты | Рекомендация | Блокирует |
|---|---|---|---|---|
| О-13 | Лимиты | число детей на родителя; проверок и разборов черновика в день; потолок на родителя | 3 ребёнка; 60 проверок и 20 разборов на ребёнка в день; на родителя — втрое больше; 3 новых профиля в день | [spec.md](spec.md), раздел 14.5 |
-| О-14 | Что видит родитель | только счётчики задач; плюс попытки и подсказки; плюс ловушки | всё перечисленное, как в сценарии С6: начатые и решённые задачи, с какой попытки, сколько было подсказок, на каких ловушках ребёнок спотыкался. Записи решения нет: черновик живёт на устройстве (R42) | API прогресса |
-| О-16 | Что после решения задачи | следующая задача; решить заново; выход | предложить следующую задачу и «решить заново» | интерфейс |
+| О-14 | Что видит родитель | только счётчики задач; плюс попытки и подсказки; плюс ловушки | всё перечисленное, как в сценарии С6: начатые и решённые задачи, с какой попытки, сколько было подсказок, на каких ловушках ребёнок спотыкался. Записи решения нет: лист живёт на устройстве (R42) | API прогресса |
+| О-16 | Что после решения задачи | следующая задача; решить заново; выход | предложить следующую задачу и «решить заново». В E2 — только Next task, после шестой задачи снова первая (R63) | интерфейс беты |
| О-17 | Как собирать сигналы о неверном чтении | кнопка у ребёнка рядом с «I read …»; кнопка в родительском разделе; только eval-наборы | кнопка «That's not what I wrote» у ребёнка, без ввода текста: просит переписать ответ и помечает проверку. Картинки не хранятся, поэтому сигнал — это только счётчик и прочитанный текст | метрику ложных тревог |
| О-18 | Тарифы после MVP (раздел 14, п. 4): размер квот и голос в тарифе за $20 | квоты — кратные лимитам беты или по бюджету на ребёнка в месяц; голос — наставник только читает подсказки вслух или ребёнок ещё и отвечает голосом | квоты — по юнит-экономике раздела 9: худший месяц подписчика должен окупаться ценой. Подписчик — семья (R07). При лимитах беты её худший месяц — ≈ $12 по средней цене вызова (30 × $0.41) и не больше ≈ $46, с 2027 года ≈ $20 и ≈ $77. Голос — только озвучка подсказок: голос ребёнка по COPPA — персональная информация, а разговора с ИИ продукт избегает (раздел 7; [privacy.md](privacy.md), раздел 10) | тарифы, уведомление для родителей |
diff --git a/docs/spec.md b/docs/spec.md
index c94aa54..31d0d1f 100644
--- a/docs/spec.md
+++ b/docs/spec.md
@@ -1,9 +1,11 @@
# canvas-api — техническая спецификация (MVP)
-> Статус: черновик для обсуждения, 2026-09-27. Собран из исходного черновика автора (в репозиторий не входит) с поправками, которые перечислены в [decisions.md](decisions.md), и исправлен по итогам ревью документов. Пересмотрен 2026-10-11 при повороте к LLM-first (R37–R43).
+> Статус: черновик для обсуждения, 2026-09-27. Собран из исходного черновика автора (в репозиторий не входит) с поправками, которые перечислены в [decisions.md](decisions.md), и исправлен по итогам ревью документов. Пересмотрен 2026-10-11 при повороте к LLM-first (R37–R43) и 2026-10-16 под эпик E2 (R60–R67).
>
> Здесь описано, как устроен сервис: стек, архитектура, модель холста, проверка ответа, разбор черновика, ИИ-наставник, API, сценарии прохождения запросов, примеры данных, модель данных, развёртывание. Что и зачем мы строим — в [product.md](product.md), данные детей — в [privacy.md](privacy.md). Открытые вопросы (Т-…) и спайки — в разделе 20.
>
+> Спецификация описывает MVP — закрытую бету, эпик E15. До неё идёт E2, минимальный срез с замером Gemini (R60): лист без поля ответа и один вызов модели (R61, R62), ключ доступа вместо входа (R64), ни базы, ни Centrifugo. Что E2 делает иначе, сказано в разделах 1 и 2 и в плашках «E2» в начале разделов. Разделы 4–8 и примеры 10.3–10.6 переписывают задачи E2, названные в плашках; разделы 3.1, 9, остальное в 10 и 12.2 остаются для беты.
+>
> Версии библиотек и сервисов указаны на 2026-09-27, цены Gemini — на 2026-10-11. При реализации они закрепляются точно: в `go.mod`, `package-lock.json` и образах с дайджестом.
## Содержание
@@ -32,15 +34,15 @@
## 1. Принципы
-- **Состояние сервера — вне процессов.** Данные приложения и хранилище OpenFGA живут в Neon, история каналов Centrifugo — в Upstash. Любой инстанс обслуживает любой запрос.
-- **Черновик — на устройстве.** Штрихи черновика и поля ответа SPA хранит в IndexedDB. По Check сервер получает две картинки и держит их только в памяти запроса (R42).
+- **Состояние сервера — вне процессов.** Данные приложения и хранилище OpenFGA живут в Neon, история каналов Centrifugo — в Upstash. Любой инстанс обслуживает любой запрос. В E2 нет ни базы, ни Centrifugo: сервер ничего не хранит, а в памяти экземпляра живёт только счётчик дневного потолка (R60, R64).
+- **Лист — на устройстве.** Поля ответа нет: решение и итоговый ответ ребёнок пишет на одном листе (R61). Штрихи листа SPA хранит в IndexedDB. По Send Answer сервер получает одну картинку — PNG листа — и держит её только в памяти запроса (R42). В E2 лист живёт на устройстве только до перезагрузки страницы (R60).
- **В простое почти бесплатно.**
- Всё работает на управляемых сервисах GCP и бесплатных тарифах SaaS (R01, R08).
- Cloud Run масштабируется до нуля, `max-instances: 1` для canvas-api и Centrifugo.
- Для роста меняется только `max-instances`, код остаётся прежним.
-- **Вердикт выносит программа, модель читает и объясняет.**
- - Gemini читает ответ на картинке (вызов 1), а верен ли он, решает программа по ключу задачи (R38). Ключа вызов 1 не видит.
- - После неверного ответа Gemini разбирает черновик и задаёт вопрос (вызов 2, R39). Этот вызов видит ключ, поэтому каждую его подсказку проверяет программа (R24).
+- **Вердикт выносит программа, модель читает и объясняет.** Своя оценка модели вердикт не решает (R37).
+ - **E2 — один вызов** (R62). Gemini получает картинку листа, условие и класс задачи, ключ и решение. Он находит итоговый ответ и читает его дважды — как написан (`written`) и голым значением (`value`), — разбирает решение и задаёт вопрос. Программа принимает `value`, только если он виден в `written`, и сверяет его с ключом; вопрос проверяет программа (R24).
+ - **Бета по плану до E2 — два вызова.** Gemini читает ответ (вызов 1), а верен ли он, решает программа по ключу задачи (R38); ключа вызов 1 не видит. После неверного ответа Gemini разбирает решение и задаёт вопрос (вызов 2, R39). Этот вызов видит ключ, поэтому каждую его подсказку проверяет программа (R24). Один вызов или два в бете решают выводы E2 (T76).
- **Ложная ошибка хуже отсутствия проверки.** При сомнении ребёнок получает просьбу переписать, при сбое — нейтральную фразу, но не ошибку (product 5.6).
- **Ядро не зависит от предмета.** Предмет — это задачи, промпт и eval-набор, а не код (R37).
- **Порты и адаптеры.** Домен — чистые функции без ввода-вывода. Внешний мир видят сценарии — ход проверки, удаление, очистка и остальные — и только через интерфейсы, которые объявляют сами (раздел 18):
@@ -51,7 +53,9 @@
- `Store` — хранилище; на деле это несколько узких интерфейсов, по одному на потребителя.
Адаптеры: Gemini — за `Recognizer` и `Tutor`, а также OpenFGA, Centrifugo, Postgres и Firebase — для входа, App Check и удаления аккаунта. Так остаётся открытым путь к k8s + EDA (раздел 19).
-- **Privacy by design.** Псевдонимы и минимум данных; в логах нет персональных данных. Во внешний ИИ уходят картинки почерка и данные задачи и проверки. Сами мы идентификаторов к ним не добавляем, но на картинке может оказаться всё, что написал ребёнок, в том числе имя ([privacy.md](privacy.md), разделы 4, 5 и 5.3).
+
+ **E2:** проверка объявляет один порт — разбор листа моделью, его реализует адаптер Gemini. `Authorizer`, `Notifier` и `Store` ей не нужны: в E2 нет ни прав, ни событий, ни базы (R60). Имя и контракт порта выбирает задача адаптера (T35).
+- **Privacy by design.** Псевдонимы и минимум данных; в логах нет персональных данных. Во внешний ИИ уходят картинка листа и данные задачи и проверки. Сами мы идентификаторов к ним не добавляем, но на картинке может оказаться всё, что написал ребёнок, в том числе имя ([privacy.md](privacy.md), разделы 4, 5 и 5.3).
- **Только точные версии.** Никаких `latest` и плавающих тегов, как в остальной экосистеме MathTrail.
## 2. Архитектура
@@ -59,8 +63,8 @@
```mermaid
flowchart LR
subgraph Device["Устройство родителя (браузер)"]
- SPA["SPA: React + Konva
лист, поле ответа, подсказки"]
- IDB[("IndexedDB
штрихи черновика и ответа")]
+ SPA["SPA: React + Konva
лист и подсказки"]
+ IDB[("IndexedDB
штрихи листа")]
end
subgraph FB["Firebase"]
Hosting["Hosting
статика SPA"]
@@ -84,12 +88,12 @@ flowchart LR
SPA -->|"загрузка"| Hosting
SPA -->|"вход"| Auth
SPA -->|"токен"| AC
- SPA -->|"HTTPS + protobuf
ID token + App Check
две PNG по Check"| API
+ SPA -->|"HTTPS + protobuf
ID token + App Check
PNG листа по Send Answer"| API
SPA <-->|"WebSocket"| CENT
API -->|"localhost"| FGA
FGA --> NEON
API --> NEON
- API -->|"PNG ответа; после ошибки —
PNG черновика и задача"| GEM
+ API -->|"PNG листа и задача"| GEM
API -->|"publish"| CENT
CENT --> UPS
API -.->|"секреты"| SM
@@ -98,23 +102,28 @@ flowchart LR
| Компонент | Где работает | Роль | Масштаб в MVP |
|---|---|---|---|
-| SPA | Firebase Hosting (CDN) | лист черновика, поле ответа и слой подсказок; черновик в IndexedDB и картинки проверки; вход, детский режим, прогресс | статика |
-| canvas-api | Cloud Run, контейнер `api` | API, проверка картинок, чтение ответа и сравнение с ключом, разбор черновика, лестница подсказок, лимиты, публикация событий | 0–1 инстанс |
+| SPA | Firebase Hosting (CDN) | лист и слой подсказок; лист в IndexedDB и картинка проверки; вход, детский режим, прогресс | статика |
+| canvas-api | Cloud Run, контейнер `api` | API, проверка картинки, чтение ответа и сравнение с ключом, разбор решения, лестница подсказок, лимиты, публикация событий | 0–1 инстанс |
| OpenFGA v1.21.0 | Cloud Run, сайдкар в том же сервисе (R09) | проверка прав (ReBAC) | вместе с canvas-api |
| Centrifugo v6.9.6 | Cloud Run, отдельный сервис | WebSocket до клиента, история канала, восстановление после разрыва | 0–1 инстанс |
| Neon Postgres | Neon, бесплатный тариф | данные приложения (`canvas`) и хранилище OpenFGA (`openfga`) | 0,5 ГБ, автопауза через 5 мин |
| Upstash Redis | Upstash, бесплатный тариф | движок Centrifugo: история, масштабирование | 500 тыс. команд в месяц |
| Firebase Auth, App Check | Firebase | вход родителя; проверка, что запрос пришёл из нашего приложения | 50 000 MAU бесплатно |
-| Gemini | Gemini Enterprise Agent Platform (бывший Vertex AI) | чтение ответа (вызов 1); разбор черновика и вопрос (вызов 2) | по запросу |
+| Gemini | Gemini Enterprise Agent Platform (бывший Vertex AI) | в E2 — один вызов: итоговый ответ, разбор и вопрос (R62); в бете по плану до E2 — чтение ответа (вызов 1), разбор и вопрос (вызов 2) | по запросу |
+
+**E2** (R60). Из этой схемы работают SPA на Firebase Hosting, canvas-api без сайдкара, Secret Manager и Gemini. Вызов API несёт ключ доступа вместо ID token и App Check (R64), а весь результат проверки приходит в HTTP-ответе (R23). OpenFGA, Centrifugo, Neon, Upstash, Firebase Auth и App Check приходят в бете.
**Периметр.**
- **MVP.** Прямые адреса Cloud Run с управляемым SSL. Защита от спама и автоматизированных запросов — Firebase App Check: без его токена canvas-api не отвечает. Centrifugo защищён тем, что подключиться можно только с JWT, который выдаёт canvas-api.
+- **E2.** Вместо App Check и ID token — ключ доступа: без него canvas-api не отвечает ни на что за `/v1`. Платные вызовы ещё ограничивает дневной потолок вызовов Gemini (R64).
- **Рост.** Global External HTTP(S) Load Balancer и Cloud Armor (ограничение частоты, WAF) перед обоими сервисами. Готового Terraform для этого в репозиториях MathTrail нет (Т-7).
## 3. Стек и библиотеки
### 3.1. Клиент (`web/`)
+> **E2:** поля ответа нет ни в E2, ни в бете (R61), а в E2 один вызов Gemini (R62). Раздел описывает бету (E15) по плану до E2, с полем ответа, и переписывается при её разбиении.
+
| Назначение | Выбор | Версия | Зачем |
|---|---|---|---|
| Язык | TypeScript | 7.0.x | типы для контрактов и холста |
@@ -145,7 +154,7 @@ flowchart LR
| Назначение | Выбор | Версия | Комментарий |
|---|---|---|---|
| Язык | Go | 1.27.2 | как в mathtrail-standalone |
-| HTTP-роутер | gin или chi | gin v1.12.0 / chi v5.3.2 | Т-1: в каркасе standalone gin (R11), в черновике chi |
+| HTTP-роутер | gin | v1.12.0 | как в каркасе standalone и mentor-api; закрывает Т-1 (R67) |
| Protobuf | `google.golang.org/protobuf` + buf | v1.36.12 | контракты в `proto/` (R12) |
| Firebase | `firebase.google.com/go/v4` | v4.22.0 | `auth.VerifyIDToken`, `VerifyIDTokenAndCheckRevoked`, `appcheck.VerifyToken`, `RevokeRefreshTokens`, `DeleteUser` (R14) |
| OpenFGA | `github.com/openfga/go-sdk` | v0.8.3 | Check, Write, Read, ReadAuthorizationModels, WriteAuthorizationModel |
@@ -176,6 +185,8 @@ flowchart LR
| Cloud Run | бесплатный лимит | 2 млн запросов, 180 000 vCPU-с, 360 000 ГиБ-с в месяц |
| Secret Manager | бесплатный лимит | 6 активных версий секретов, 10 000 обращений в месяц — на весь платёжный аккаунт, а не на проект |
+В E2 вызов один, по умолчанию на Flash; модели и настройки для беты выбирает eval E2 на Flash-Lite, Flash и Pro, а решают его выводы (R22, R62, T76).
+
Цены Gemini сверены 2026-10-11. У моделей Gemini 3 картинка стоит не больше токенов, чем позволяет параметр `media_resolution`: 1 120 по умолчанию, 280 на уровне `low` (раздел 7.1, product 9).
Vertex AI 22.04.2026 переименован в Gemini Enterprise Agent Platform. Вызовы через SDK Vertex AI продолжают работать. В коде бэкенд по-прежнему называется `BackendVertexAI`. Gemini 2.5 Flash не используем: его снимают 16.10.2026 (R22).
@@ -189,6 +200,8 @@ Vertex AI 22.04.2026 переименован в Gemini Enterprise Agent Platfor
## 4. Модель холста и координат
+> **E2:** лист без поля ответа, один вызов Gemini — решения E2 (R61, R62). Раздел переписывает T67.
+
### 4.1. Логическая страница
Все координаты хранятся в **логических единицах**, а не в пикселях экрана (R15). У листа черновика и у поля ответа свои системы координат: начало — в левом верхнем углу, ось y направлена вниз. Размеры — константы протокола (Т-16).
@@ -277,6 +290,8 @@ w = (xmax − xmin) / 1000 × W h = (ymax − ymin) / 1000 × H
## 5. Проверка ответа
+> **E2:** лист без поля ответа, один вызов Gemini — решения E2 (R61, R62). Раздел переписывает T66.
+
Пакет `internal/domain/answer`: чистая логика без ввода-вывода (R38). Картинку ответа читает вызов 1 через порт `Recognizer` (раздел 7). Пакет получает прочитанное и выносит вердикт.
### 5.1. Вход и выход
@@ -377,6 +392,8 @@ w = (xmax − xmin) / 1000 × W h = (ymax − ymin) / 1000 × H
## 6. Разбор черновика
+> **E2:** лист без поля ответа, один вызов Gemini — решения E2 (R61, R62). Раздел переписывает T34.
+
Пакет `internal/domain/tutor`: что происходит после вердикта, лестница подсказок, проверки вопроса и запасные тексты. Черновик разбирает вызов 2 через порт `Tutor` (раздел 7). Вердикт к этому моменту уже вынесен и не меняется: вызов 2 не может сделать верный ответ неверным (R39).
### 6.1. Маршрут после вердикта
@@ -431,6 +448,8 @@ w = (xmax − xmin) / 1000 × W h = (ymax − ymin) / 1000 × H
## 7. ИИ-наставник
+> **E2:** лист без поля ответа, один вызов Gemini — решения E2 (R61, R62). Раздел переписывает T35.
+
### 7.1. Модели и версии
| Вызов | Модель | `thinkingLevel` | `maxOutputTokens` | Картинка, `media_resolution` | Тайм-аут |
@@ -582,6 +601,8 @@ Previous questions on this sheet: none
## 8. API canvas-api
+> **E2:** лист без поля ответа, один вызов Gemini — решения E2 (R61, R62). Раздел переписывают задачи E2: 8.2 — T18, 8.6 — T19, остальное — T70.
+
### 8.1. Транспорт и заголовки
- HTTPS, тело — protobuf (`Content-Type: application/x-protobuf`). При локальном запуске (`CANVAS_ENV=local`) сервер также принимает и отдаёт JSON по правилам protojson — для отладки. Значение `dev` оставлено для второй среды после MVP (R32).
@@ -896,6 +917,8 @@ message CreateCheckResponse {
## 9. Сценарии прохождения запроса
+> **E2:** поля ответа нет ни в E2, ни в бете (R61), а в E2 один вызов Gemini (R62). Раздел описывает бету (E15) по плану до E2, с полем ответа, и переписывается при её разбиении.
+
Оценки задержек в этом разделе — гипотезы. Их подтверждает спайк S4.
### 9.1. Вход родителя и профиль ребёнка
@@ -1148,6 +1171,8 @@ sequenceDiagram
## 10. Примеры данных: полный цикл
+> **E2:** поля ответа нет ни в E2, ни в бете (R61), а в E2 один вызов Gemini (R62). Примеры 10.3–10.6 переписывает T35 под один вызов. Остальное описывает бету (E15) по плану до E2, с полем ответа, и переписывается при её разбиении.
+
Задача про лестницу: ребёнок пишет 6, получает «Not yet» и вопрос с рамкой. Это его первая проверка на холсте. Идентификаторы вымышленные.
### 10.1. Клиент → canvas-api
@@ -1605,6 +1630,8 @@ erDiagram
### 12.2. Детский режим и PIN
+> **E2:** поля ответа нет ни в E2, ни в бете (R61), а в E2 один вызов Gemini (R62). Раздел описывает бету (E15) по плану до E2, с полем ответа, и переписывается при её разбиении.
+
- Родитель выбирает профиль, и SPA переходит в детский режим: задачи, лист и поле ответа, без настроек, прогресса и удаления.
- Для выхода нужен PIN из 4–6 цифр. Проверка — `POST /v1/me/pin:verify`, не больше 5 попыток за 15 минут.
- Счётчик неудачных попыток и время блокировки хранятся в `parents` (`pin_failed_count`, `pin_locked_until`), а не в памяти инстанса.
@@ -1788,9 +1815,9 @@ canvas-api подписывает оба токена алгоритмом HS256
infra/terraform/
versions.tf провайдеры и их точные версии: google и google-beta (Firebase — только в нём)
backend.hcl состояние в GCS
- apis.tf включение API (Run, Artifact Registry, IAM и WIF, Firebase, бюджеты, Monitoring, Logging, Observability; Secret Manager, Agent Platform и reCAPTCHA Enterprise — с задачами эпика MVP)
+ apis.tf включение API (Run, Artifact Registry, IAM и WIF, Firebase, бюджеты, Monitoring, Logging, Observability; Secret Manager и Agent Platform — с T64 в E2, reCAPTCHA Enterprise — в бете)
registry.tf Artifact Registry для образов
- secrets.tf семь секретов (значения кладутся вне ресурсов Terraform; R34)
+ secrets.tf в E2 — секрет ключа доступа (T64, R64); в бете — семь секретов; значения кладутся вне ресурсов Terraform (R34)
iam.tf аккаунты сервиса, деплоя и выкладки SPA, их роли; кто их одалживает — по окружению GitHub (R58)
service_api.tf Cloud Run: canvas-api + сайдкар OpenFGA
service_centrifugo.tf Cloud Run: Centrifugo
@@ -1805,7 +1832,7 @@ infra/terraform/
```
- **Neon и Upstash** подключаются через их провайдеры Terraform, где это возможно. Иначе ресурсы создаются вручную, а строки подключения кладутся в Secret Manager.
-- **Gemini.** Аккаунт сервиса получает роль вызова моделей. Кеширование входов модели выключается для проекта до первой проверки ребёнка ([privacy.md](privacy.md), раздел 5.2). По документации это вызов API, а не настройка Terraform, поэтому его делает рецепт выкладки; способ сверяется с документацией перед реализацией.
+- **Gemini.** Аккаунт сервиса получает роль вызова моделей. Кеширование входов модели выключается для проекта до первого вызова Gemini ([privacy.md](privacy.md), раздел 5.2) — ресурсом Terraform `google_vertex_ai_cache_config` с `disable_cache = true`, а не рецептом выкладки. Ресурс есть в провайдере `google` 8.3.0 (R62). Права, которые ему нужны, и его поведение на живом проекте сверяет T64; если ресурс не подойдёт, выключение возвращается в рецепт.
- **Образы — не дело Terraform.** Как в mathtrail-standalone, Terraform создаёт сервисы и jobs с начальным образом и игнорирует изменения образа (`lifecycle.ignore_changes`). Образы выкатывает CD (15.5).
- **Трафик — тоже.** Блок `traffic` в сервисах не задаётся, а если провайдер его всё же сверяет, изменения игнорируются. Иначе `terraform apply` следующей выкладки снял бы закрепление трафика после отката раньше, чем выйдет новый образ. Сверено в T10: в провайдере 8.3.0 `traffic` — Optional и Computed, и без блока расхождений нет (R56).
- **`prod.auto.tfvars` публичен** (репозиторий открытый). В нём нет ни секретов, ни персональных данных.
@@ -1972,7 +1999,7 @@ draft/ макет экрана: предложение, а н
| `proto/canvasapi/v1` | `contracts/proto/canvas/v1` | согласовать поля (ниже) |
Что расходится с `contracts/proto/canvas/v1` сейчас:
-- платформа ждёт поток штрихов (`CanvasStrokeEvent`), а MVP шлёт по Check две картинки и не отдаёт штрихов с устройства (R42);
+- платформа ждёт поток штрихов (`CanvasStrokeEvent`), а MVP шлёт по Send Answer одну картинку листа и не отдаёт штрихов с устройства (R42, R61). Слать ли штрихи, решает исследование E2 (R66);
- `HintEvent.rect` в пикселях холста, а наша рамка — в логических единицах листа (4.4);
- `CanvasStrokeEvent.user_id` — идентификатор Kratos ученика, а у нас субъект — родитель, а ребёнок определяется через холст;
- `hint_type` — `warning`/`success` вместо наших вердиктов и причин.
@@ -1992,26 +2019,25 @@ draft/ макет экрана: предложение, а н
| № | Вопрос | Варианты | Рекомендация | Блокирует |
|---|---|---|---|---|
-| Т-1 | HTTP-роутер | gin (каркас standalone и mentor-api); chi (черновик) | gin: каркас переиспользуется без переделки | каркас сервиса |
-| Т-5 | ID моделей по вызовам | чтение ответа: 3.5 Flash-Lite, 3.1 Flash-Lite или 3.8 Flash; разбор черновика: 3.8 Flash (R22) | по итогам S1, S2 и eval-наборов | конфигурацию |
-| Т-6 | Клетка на картинке черновика (4.3) | с клеткой — модели проще назвать место; без клетки — меньше линий, которые не писал ребёнок | решить по S2 | картинки проверки |
+| Т-5 | ID моделей по вызовам | чтение ответа: 3.5 Flash-Lite, 3.1 Flash-Lite или 3.8 Flash; разбор черновика: 3.8 Flash (R22). В E2 — один вызов, по умолчанию Flash (R62) | eval E2 гоняет журнал автора на Flash-Lite, Flash и Pro, а модели, настройки и один вызов или два для беты решают выводы E2 (T76) | конфигурацию беты |
+| Т-6 | Клетка на картинке листа (4.3) | с клеткой — модели проще назвать место; без клетки — меньше линий, которые не писал ребёнок | решить при разбиении E15: S2 заменил эксперимент E2, а он клетку не меряет — лист E2 рисуется без клетки (T48) | картинку проверки беты |
| Т-7 | Terraform для GLB + Cloud Armor | в репозиториях MathTrail не найден | нужна ссылка от автора или новая задача | вариант роста |
| Т-11 | Сроки проверки и аренды. 25 с на весь запрос и 40 с аренды (8.4) — оценки: оба вызова Gemini с ожиданием слотов, пробуждение Neon и проверка отзыва токена должны в них уложиться | оставить; срок меньше и тайм-ауты вызовов короче; срок больше вместе с тайм-аутом Cloud Run | оставить и проверить замером нагрузки и спайком S4. Если не укладывается, сначала укорачивать ожидание слотов, а не тайм-ауты вызовов | проверку ответа, клиент |
| Т-12 | Клиент ушёл посреди проверки, до первого байта ответа. Проверка после аренды идёт на контексте без отмены (8.4). Образец проверил живым прогоном, что Cloud Run держит запрос и процессор, пока не вернулся обработчик, — но для клиента, ушедшего уже после ответа (R255 standalone). Наш случай — тот, где ответа ещё не было | не обрывать проверку; обрывать | не обрывать (8.4). Поведение Cloud Run в нашем случае проверить на живом сервисе до кода проверки | проверку ответа |
| Т-14 | Слоты к Gemini и к раскодированию картинок. При параллельности 40 (15.3) одновременные вызовы ничем не ограничены, и всплеск упирается в квоты Gemini. Раскодирование держит до ≈ 10 МБ на запрос: раскодированные картинки, их серые копии и тело запроса (4.3), а у экземпляра 1 ГиБ на два контейнера | слот на каждый вид работы — чтение, разбор, раскодирование — с ограниченным ожиданием, как R122 standalone; только тайм-ауты и лимит частоты | три вида слотов с ограниченным ожиданием. Кто не дождался слота чтения, получает `UNCHECKED` · `busy`, слота разбора — запасной текст (9.9). Раскодирование идёт ещё до аренды (8.4), поэтому без его слота ответ — 503 `UNAVAILABLE`, и клиент повторит запрос; числа — по замеру нагрузки | проверку ответа, разбор черновика |
| Т-15 | Пулы соединений против параллельности. Пул pgx сервиса, пул OpenFGA к своей базе и HTTP-клиенты адаптеров не согласованы ни с параллельностью 40, ни с лимитом соединений Neon — прямых и через pooler | пул по параллельности; пул меньше, с ограниченным ожиданием; pooler Neon | ожидание соединения ограничено сроком, исчерпанный пул — `UNAVAILABLE` (8.3, 9.9); сроки запросов и блокировок — на стороне базы; размеры и pooler — по замеру нагрузки и документации Neon | Postgres, OpenFGA |
-| Т-16 | Геометрия листа и картинок проверки: размеры листа и поля ответа, размер PNG и масштаб, толщина линии, пределы объёма | значения 4.1 и 4.3; крупнее или мельче — по данным S1 и S2 | оставить значения 4.1 и 4.3, если S1 и S2 не покажут иного, и закрепить их до контрактов | контракты эпика MVP (proto), образцы спайков S1 и S2 |
+| Т-16 | Геометрия листа и картинки проверки: размер листа, размер PNG и масштаб, толщина линии, пределы объёма. Поля ответа больше нет (R61) | книжный лист из 4.1 и 4.3; лист E2 — один экран без прокрутки, альбомный, как планирует T67 | закрепить в T67 константами Go — один источник, который клиент получает в `ListTasksResponse` (R61). Тогда вопрос закрыт | контракты E2 (T18), образцы и eval E2 |
-Т-10 (как публиковать модель OpenFGA) решён: модель применяет canvas-api при старте (R28, раздел 12.3). Т-2, Т-4, Т-8 и Т-9 сняты 2026-10-11: разбора LaTeX, Mathpix, сегментации строк и штрихов на сервере больше нет (R37, R42). Т-13 решён тогда же: проверку, которую никто не завершил, держит и доводит аренда (R42, 8.4). Т-3 (регион) решён в T09: `us-east4` (R54). Т-17 (кто публикует образ тулчейна CI) решён в T08: `main` проверяет только в образе, который сам подписал cosign, а образ без его подписи собирает заново (R53).
+Т-1 (HTTP-роутер) решён 2026-10-15 при планировании E2: gin, как в каркасе standalone (R67). Т-10 (как публиковать модель OpenFGA) решён: модель применяет canvas-api при старте (R28, раздел 12.3). Т-2, Т-4, Т-8 и Т-9 сняты 2026-10-11: разбора LaTeX, Mathpix, сегментации строк и штрихов на сервере больше нет (R37, R42). Т-13 решён тогда же: проверку, которую никто не завершил, держит и доводит аренда (R42, 8.4). Т-3 (регион) решён в T09: `us-east4` (R54). Т-17 (кто публикует образ тулчейна CI) решён в T08: `main` проверяет только в образе, который сам подписал cosign, а образ без его подписи собирает заново (R53).
**Спайки перед основной реализацией:**
| № | Что проверяем | Как | Критерий успеха |
|---|---|---|---|
-| S1 | Чтение ответа Gemini (вызов 1) | 300+ ответов, написанных взрослыми в поле ответа геометрии Т-16: числа, в том числе нарочно «детские» 1 и 7, 4 и 9, 5 и S; ответы-слова; пустые поля, каракули, два числа, надписи-инструкции. Модели — по Т-5, на нескольких уровнях `media_resolution`. Детские образцы — только по П-10 ([privacy.md](privacy.md), раздел 4). Меряем ошибки чтения, p95 задержки и цену | ложный `INCORRECT` ≤ 1%, ложный `CORRECT` ≤ 0,5%, лишний `UNREADABLE` ≤ 5% (цели product 12); p95 вызова ≤ 1,2 с — остальным этапам до 2 с product 10 нужно ≈ 0,8 с (14.1) |
-| S2 | Разбор черновика Gemini (вызов 2) | 100+ черновиков, написанных взрослыми, с типичными ошибками задач `tasks/`, в геометрии Т-16, с клеткой и без (Т-6), на нескольких уровнях `media_resolution` | рамка на месте ошибки ≥ 80%, не на месте ≤ 10%; рубрику проходят ≥ 95% вопросов, утечек ключа нет (цели product 12); p95 вызова ≤ 3,5 с — с запасом до 6 с product 10 (14.1) |
+| S1 | Чтение ответа Gemini (вызов 1) | **Заменён экспериментом E2** (R60): живой прогон на образцах автора (T75), eval его журнала на Flash-Lite, Flash и Pro — в том числе вариант «только чтение», первый вызов двухвызовной схемы (T74) — и выводы (T76). Детские образцы — только по П-10 ([privacy.md](privacy.md), раздел 4) | цели беты, а не E2: ложный `INCORRECT` ≤ 1%, ложный `CORRECT` ≤ 0,5%, лишний `UNREADABLE` ≤ 5% (цели product 12). Сотня листов E2 — разведка с доверительными интервалами (≤ 5%), а не проверка этих целей (T74); p95 вызова ≤ 1,2 с — остальным этапам до 2 с product 10 нужно ≈ 0,8 с (14.1) |
+| S2 | Разбор черновика Gemini (вызов 2) | **Заменён экспериментом E2** (R60): разбор листа — часть единственного вызова E2, его мерят тот же живой прогон, eval и выводы (T74–T76) | цели беты, а не E2: рамка на месте ошибки ≥ 80%, не на месте ≤ 10%; рубрику проходят ≥ 95% вопросов, утечек ключа нет (цели product 12); p95 вызова ≤ 3,5 с — с запасом до 6 с product 10 (14.1) |
| S3 | Centrifugo на Cloud Run | неделя тестового трафика: переподключения, стоимость инстанса, расход команд Upstash | стоимость и команды в пределах бюджета беты |
-| S4 | Сквозная задержка | проверка с верным и с неверным ответом на тёплом и холодном пути (Cloud Run, Neon) | вердикт ≤ 2 с, вопрос с рамкой ≤ 6 с от Check (p95), холодный старт ≤ 3 с (product 10) |
+| S4 | Сквозная задержка | проверка с верным и с неверным ответом на тёплом и холодном пути (Cloud Run, Neon) | вердикт ≤ 2 с, вопрос с рамкой ≤ 6 с от Send Answer (p95), холодный старт ≤ 3 с (product 10) |
## 21. Источники
diff --git a/docs/stack.html b/docs/stack.html
index 322c69c..6e4d350 100644
--- a/docs/stack.html
+++ b/docs/stack.html
@@ -256,7 +256,7 @@
canvas-api · MVP · закрытая бета
+canvas-api · E2 — срез с замером Gemini · E15 — закрытая бета
Что работает на каждом слое — от холста в браузере до Terraform. Собрано из spec 2, 3 и 12–20, журнала решений и плана RUN.md. Версии библиотек и сервисов — ориентиры spec 3 на 2026-09-27, версии инструментов — из .devcontainer/Dockerfile. Задача, которая подключает технологию, закрепляет её версию точно: в go.mod, lock-файле или по digest.
.devcontainer/ (Dockerfile, devcontainer.json), в go.mod, в web/package.json с его lockfile, в justfile или в .github/. Здесь — её снимок на дату внизу страницы.Вопросы spec 20 и спайки, от которых зависит выбор технологий. Остальные вопросы spec 20 — Т-11, Т-12 и Т-14 — о поведении сервиса, а не о стеке.
Spec называет варианты и отдаёт выбор задаче, которая пишет этот код. Отдельного вопроса Т-… для них нет.
net/http (spec 3.2).net/http (spec 3.2).app.NewContainer, как в mentor-api. CLAUDE.md, Wiring