Appearance
Teacher AI Preview Gateway Rehearsal
Date: 2026-07-12
Scope
This proof exercises only the opt-in teacher preview manifests:
deploy/gateway/routes.teacher-ai-preview-native-example.jsondeploy/gateway/routes.teacher-ai-preview-native-localhost-example.json
The same manifests route two reviewed Question Bank writes after preview:
POST /api/questions/ai-preview-drafts->POST /v1/questions/ai-preview-draftsPOST /api/questions/ai-preview-solutions->POST /v1/questions/ai-preview-solutions
Both Question Bank writes require the same authenticated ADMIN/TEACHER tenant scope and remain exact non-default routes. The solution write appends a canonical Question Bank version only after teacher review; it is not an AI service write.
The default deploy/gateway/routes.json is unchanged. Its broad /api/questions route remains legacy_proxy.
The same opt-in manifests also contain one exact no-write attempt preview:
POST /api/attempts/ai-essay-grade/preview->POST /v1/ai/essay-grade/preview
It requires the same authenticated ADMIN/TEACHER tenant scope. It returns only an advisory rubric preview, and never calls an attempt handoff or records a score.
Runtime Setup
- Ran the service-owned schema migrator:
docker compose -f deploy/docker-compose.yml run --rm ai-classifier-service-migrations. - Started
ai-classifier-serviceand confirmedGET http://127.0.0.1:8103/healthzreturned200. - Started a temporary
api-gatewayon:18091with the localhost preview manifest,AUTH_JWT_SECRET=runtime-gateway-secret, and a deliberately unreachable legacy upstream. The temporary listener was stopped after the smoke.
Results
| Request | Result |
|---|---|
Teacher POST /api/questions/ai-generation/generate | 200, X-Hoctapaz-Gateway-Route=teacher-ai-generation-preview, native_write, operation=generate, status=PREVIEW, provider mock, chargePolicy=preview_only_no_consume. |
Teacher POST /api/questions/ai-solutions/editor | 200, X-Hoctapaz-Gateway-Route=teacher-ai-explanation-preview, native_write, operation=explain, status=PREVIEW, provider mock, chargePolicy=preview_only_no_consume. |
| Student generation preview | 403 AUTH_FORBIDDEN. |
| Teacher preview without default organization | 400 TENANT_REQUIRED. |
The first smoke returned 500 because the local Postgres instance had not run the service-owned provider_usage_events migration. Re-running through the Compose migration service applied 000001 through 000003; the repeated requests above then passed without changing application behavior.
Static And Hermetic Coverage
bash
pnpm --filter @hoctapaz/ui typecheck
pnpm --filter @hoctapaz/ui test -- --run packages/ui/src/modules/teacher/teacher-workspace.test.tsx
GOTOOLCHAIN=go1.25.11 go test ./services/api-gateway/internal/gateway -run TeacherAIPreview -count=1
make test-teacher-ai-preview-routes
make test-ai-classification-job-route-guardThe frontend invokes the preview Gateway only with its stored Gateway session and ?teacherAiPreviewGateway=native; otherwise it keeps the preview local. For the similar generation mode, the Teacher UI requires one to five canonical Question Bank IDs before it sends the request. It sends mode=similar and referenceQuestionIds; ai-classifier-service then resolves the tenant-scoped reference context from Question Bank instead of accepting reference content from the browser. Missing or oversized IDs are rejected in the UI before an AI or Gateway request is made. When the teacher approves a similar-preview candidate, the Question Bank draft retains referenceQuestionId and referenceQuestionIds in sourceMetadataJson, so the reviewed canonical draft remains traceable to its tenant-scoped Question Bank source. The separate classification-job reader requires ?teacherAiClassificationGateway=native. The two preview requests do not write canonical data themselves. After teacher review, the authenticated UI may submit the reviewed candidate through the exact Question Bank draft or solution owner-write route; AI service never performs that write.
Essay Grade Candidate UI Contract
/teacher/results/ai-essay-grade shows a review-only AI rubric preview. With an authenticated Gateway session and ?teacherAiPreviewGateway=native, the UI calls only POST /api/attempts/ai-essay-grade/preview; the result remains an advisory score and feedback for teacher review. Without that candidate query, the UI prepares a local preview and makes no network request. The page has no score-save or attempt-handoff action.
The current evidence is hermetic: the Teacher UI test verifies the local and authenticated candidate paths, the Gateway test verifies route scope and target mapping, and the route guards validate both opt-in manifests. It is not a live candidate Gateway smoke or default-route promotion.
Similar Preview Runtime Evidence
On 2026-07-12, a temporary SINGLE_CHOICE question was created through question-bank-service under an isolated QA organization. A direct POST /v1/ai/generation/preview request with mode=similar and that question ID returned two PREVIEW candidates with generationMode=similar, the service-owned question-service handoff, and chargePolicy=preview_only_no_consume. The only question-bank row was the explicit fixture; AI did not create a canonical question.
The same reference ID under another organization returned 400 AI_CLASSIFIER_INVALID because question-bank-service did not authorize the reference context. The fixture question and its one provider usage event were removed after the check. This proves the scoped reference boundary for the mock preview, not provider-backed generation, quota consumption, credit debit, or default Gateway promotion.
Rollback
Run Gateway with the default routes.json, or remove the three named preview entries from the opt-in manifest. The two question paths then use the broad legacy question route, while the essay-grade path has only the broad legacy /api fallback. No default route promotion, provider credential, quota consume, attempt handoff, stored score, or canonical question/solution write is included in this rehearsal.