Spec 15 Assumed API Surface - SekiroKenjii/Quizzivy GitHub Wiki
Section of Quizzivy โ Frontend Portal & Data Model Specification ยท Previous ยท Next
Base VITE_API_BASE_URL, JSON, Authorization: Bearer <access>.
# public (unauthenticated โ rate-limited, ยง6.5)
GET /public/status โ {maintenance: {startsAt, endsAt, active} | null, ai}
same answer for everyone, Cache-Control public 30 s,
120/min and 2,000/h per IP; ai is true when the AI
assistant is configured, read from configuration and
never from the provider
POST /join/preview {joinCode} โ {classId, className, teacherName}
POST /auth/login {email,password} โ {accessToken,user: CurrentUser}
POST /auth/google {code,codeVerifier,redirectUri,joinCode?} โ {accessToken,user: CurrentUser}
POST /auth/refresh (cookie) โ {accessToken}
# authenticated
POST /auth/logout
GET /auth/me โ CurrentUser
PATCH /auth/me {fullName?,displayName?|null,phone?|null,locale?,timeZone?}
โ CurrentUser; at least one supplied field;
omitted fields unchanged; unknown zone400 VALIDATION_FAILED
PATCH /me/preferences top-level UserPreferences merge โ UserPreferences;
nested assignmentDefaults and ai {language, tagDrafts,
readStudentAnswers, readImports} replace their key, {} no-op;
raw/stored UTF8 cap8192 bytes,400 on excess
GET /me/notifications?before=&limit=&audience= โ {items: [Notification], nextBefore}; the caller's own,
newest first (20 by default, 50 at most), of one console's
kinds when audience is teacher or student. Writes the caller's
due-time notifications first, at most once in 5 minutes
POST /me/notifications/read {ids?, audience?} (1 to 100; absent means all; audience narrows
both to that console's kinds) โ 204 whatever matched
GET /me/summary?audience= โ {unreadNotifications}, of one console's kinds when audience
is given; writes the due-time notifications first
GET /me/notification-preferences โ the five switches [{event,inApp,email}]
PUT /me/notification-preferences the five switches, each once โ the stored switches
PUT /me/avatar multipart file (PNG or JPEG, at most 2 MiB, each side 200โ2048 px,
a JPEG at most 32 scans) โ CurrentUser; stored as a 256ร256 PNG,
EXIF orientation applied then dropped; 413 MEDIA_TOO_LARGE, 415
MEDIA_TYPE_UNSUPPORTED | MEDIA_UNREADABLE | IMAGE_DIMENSIONS;
10 an hour per user
DELETE /me/avatar โ CurrentUser without avatarUrl
POST /auth/change-password 400 VALIDATION_FAILED on the rules (ยง5.4), 400 PASSWORD_UNCHANGED
POST /auth/google/link link Google to current account โ CurrentUser
DELETE /auth/google/link rejected if it would leave no login method
GET /auth/sessions โ {items: [Session]}: the caller's live sessions, at most 100, the one the
refresh cookie names first; {familyId, device|null, deviceKind, location|null,
lastUsedAt, current}. Without a cookie that names one, none is current
DELETE /auth/sessions/{familyId} โ 204; 404 for another user's, revoked, expired or unknown id, 409 SESSION_IS_CURRENT
for the calling session, 401 when no cookie names the caller's session;
moves the session epoch; 10/min and 60/h per user
POST /auth/sessions/revoke-others โ {revoked}; 401 as above; moves the epoch only when revoked > 0;
5/min and 30/h per user
# teacher
GET /teacher/dashboard?range=7d|14d|30d โ Dashboard (default 14d; own teaching)
GET /teacher/summary?audience= โ TeacherSummary {liveAssignments,answersToGrade,unreadNotifications};
the count is of one console's kinds when audience is given
GET /teacher/tests?status=&tag=&q=&page=&limit=
POST /teacher/tests | GET /:id | PATCH /:id
POST /teacher/tests/:id/publish {changeNote?} (at most 200 characters; blank or null is none) โ 201 TestVersion;
its number is the `nextVersion` the test reported, and the answer alone
carries testUpdatedAt, the test's updatedAt after the publish, which a
caller that goes on saving sends as expectedUpdatedAt or gets STALE_WRITE
GET /teacher/tests/:id/versions โ the versions, newest first: {version, totalPoints, questionCount, audioCount,
manualCount, skills, publishedAt, publishedBy, assignmentCount, changeNote}
GET /teacher/tests/:id/versions/:version/diff?against=previous|draft|<n>
โ {from, to, changes}; reads only, 400 for the version itself, 404 for a missing
version or another teacher's test, 422 when a group cannot be read back
POST /teacher/tests/:id/duplicate
POST /teacher/tests/:id/questions/move {expectedUpdatedAt, questionId, to: {sectionId, groupId|null, index},
expectedGroupRevisions: [{groupId, revision}]} (one or two, once each,
exactly the groups the move touches) โ {test, questionId, copied, restored, editedCopy,
groups};
content.tests.write, at most 4 KiB. index counts once the question has
left its old place; past the end is the end. A standalone question to a
group is copied (copied true, a new questionId, which remembers the bank
question); a member out of a group goes back as the original when it is an
unedited copy of a bank question still in the bank (restored true,
questionId the original's), otherwise keeps its id (editedCopy true when it
was an edited copy of a question still there); a member to another group or
within one keeps its id. A move between two standalone
places is updateTest's. 400 VALIDATION_FAILED with details.rule
move_outline_only | move_group_section | move_revisions |
move_source_ambiguous; 404 for a test, question, section or group the
caller cannot reach or that is not of this test, an expectedGroupRevisions
entry included, before any revision is compared; 409 STALE_WRITE (the test or
a touched group is behind, or a touched group is missing from the list) |
TEST_ARCHIVED | GROUP_CONFLICT; 413 over 4 KiB; 422 VALIDATION_FAILED with
details.rule group_gap_bound (and stimulusId, gapId, questionId) |
group_limits | group_audio_scope | group_asset | group_reference.
Nothing is written on a refusal
GET /teacher/questions?type=&tag=&tagMatch=any|all&level=&skill=&hasAudio=&q=&page=&limit=
POST /teacher/questions | PATCH /:id | DELETE /:id
an audio policy carries allowPause (ยง11.1): optional in a request,
where a create stores true and an update keeps the stored value;
always sent in an answer. The group writes follow the same rule
options[].id and blanks[].id: sent with an id the question holds, the row is
updated in place; sent without one, or with a foreign id, it is created; not
sent, it is deleted. A create ignores the ids it is sent
POST /teacher/media?defaultMaxPlays= multipart โ MediaAsset (validates mime, size, duration, quota)
GET /teacher/media?kind=&unused=&q=&page=&limit= rows, totalBytes, facets and usage (library, held and pending bytes) against the quota
PATCH /teacher/media/:id {displayName?, defaultMaxPlays?} โ the library row
POST /teacher/media/:id/replace multipart โ {asset, repointed: {questions, groups}, left: {questions, groups}, previous};
415 MEDIA_KIND_MISMATCH for a file of the other kind (ยง11.1); 429 past ten a minute
DELETE /teacher/media/:id 409 if referenced by a published version; 200 โ {id, bytes, url, usedIn, deleteAt}
GET /teacher/assignments?status=&classId=&classId=&q=&page=&limit= | POST | GET /:id | PATCH /:id
rows carry questionCount; classId repeats (any of them)
PATCH 409 ASSIGNMENT_LOCKED while the assignment is open and the
body changes testVersionId, durationMinutes or maxAttempts;
PATCH 409 ASSIGNMENT_CLOSED when the assignment is closed and
the window would open it again (use /reopen);
PATCH 400 VALIDATION_FAILED naming closeNow when closeNow is
sent with opensAt still ahead (a POST ignores closeNow)
POST /teacher/assignments/:id/extend {minutes,notify?} โ Assignment; 409 ASSIGNMENT_CLOSED when closed
POST /teacher/assignments/:id/duplicate {classIds} โ 201 Assignment, a draft; 409 TEST_NOT_PUBLISHED
GET /teacher/assignments/:id/item-analysis โ {handedIn, items:[{questionId,number,type,promptExcerpt,
answered,correctRate|null}]}, hardest first
GET /teacher/assignments/results.csv?ids= โ text/csv, BOM; 404 for any id not reached; 422 over 20000 rows
GET /teacher/assignments/:id/student-overrides โ {items}
PUT /teacher/assignments/:id/student-overrides {studentIds,extendBy?|closesAt?,durationMinutes?,
extraAttempts?,reason,notify?} โ {items}; 422 for a student
who is not a target the caller reaches; 409 ASSIGNMENT_CLOSED
for extendBy on a student whose close has passed
DELETE /teacher/assignments/:id/student-overrides/:studentId
GET /teacher/assignments/:id/attempts โ rows incl. integrity + audio summary; a row carries extendedTo
when the student's own override closes later than the assignment's
effective close, and not otherwise
POST /teacher/attempts/:id/extend {minutes,reason}
POST /teacher/attempts/:id/reset {reason}
POST /teacher/attempts/:id/void {reason}
GET /teacher/attempts/:id | GET /teacher/attempts/:id/events
POST /teacher/attempts/:id/grade {items:[{questionId,points,comment}]}
POST /teacher/attempts/:id/finish-grading
GET /teacher/grading/queue?mode=student|question&assignmentId=&studentId=
โ {groups,items,answersRemaining,studentsWaiting}; default student
GET /teacher/students?q=&classId=&classId=&status=&mustChangePassword=&page=&limit= | POST | GET /:id
PATCH /teacher/students/:id 403 FORBIDDEN when disabled is sent without
people.users.manage or the subset rule refuses; 403 STUDENT_SHARED
for a new email when someone else also reaches the student (ยง5.4)
POST /teacher/students/reset-passwords {studentIds (1-40)}
โ 200 {items:[{studentId,fullName,email,temporaryPassword}],
failed:[{studentId,code}]}, each in the order asked, Cache-Control
no-store; code NOT_FOUND | FORBIDDEN | STUDENT_SHARED | INTERNAL (ยง5.4)
POST /teacher/students/:id/reset-password
403 STUDENT_SHARED when someone else also reaches the student,
403 FORBIDDEN when the subset rule refuses (ยง5.4)
GET /teacher/classes?q=&page=&limit=&status=&withCodes=
rows carry joinCode in full, its code opened only with
withCodes=true for a holder of teaching.classes.write and
only where getJoinCode would open it; Cache-Control no-store
POST /teacher/classes {name, description, scheduleLabel, room, selfJoinEnabled}
GET /teacher/classes/:id
PATCH /teacher/classes/:id {name, description, scheduleLabel, room, selfJoinEnabled, archived};
an omitted scheduleLabel or room keeps its value, null or blank clears it
DELETE /teacher/classes/:id archived only, else 409 RESOURCE_NOT_ARCHIVED;
409 RESOURCE_REFERENCED {referencedBy}
POST /teacher/classes/:id/members | DELETE /teacher/classes/:id/members/:userId
GET /teacher/classes/:id/join-code โ {code, hint, legacy, expiresAt, maxUses, usesCount}
Cache-Control no-store; code null for a legacy code or one
sealed under a key the server no longer holds; 404 without
an active code
POST /teacher/classes/:id/join-code rotate {expiresInDays, maxUses} โ {code, expiresAt, maxUses};
expiresAt is 23:59:59 of the last day in the caller's zone (ยง6.1);
409 CLASS_ARCHIVED for an archived class, nothing written (ยง6.4)
DELETE /teacher/classes/:id/join-code revoke, disable self-join
# teacher: Word imports (content.tests.write; the caller's own imports, ยง16)
GET /teacher/imports/capabilities โ {intakeEnabled, processingEnabled, retention}
GET /teacher/imports/limits โ {maxBytes, formats, pasteMaxCharacters}
POST /teacher/imports {requestId, title, readWithAi?} โ 201 WordImport, idempotent on requestId;
422 VALIDATION_FAILED naming readWithAi when it is true and the server
has no assistant or the caller lacks ai.use (ยง16)
GET /teacher/imports?status=&status=&q=&page=&limit= โ {items: [WordImportHistoryItem], facets}; every item
carries reviewCounts {needsAction, toConfirm} or null
GET /teacher/imports/:id โ WordImport with its current sources
POST /teacher/imports/:id/sources multipart .docx | .doc | .pdf โ 201 ImportUploadReceipt
POST /teacher/imports/:id/sources/text {uploadId, expectedRevision, text} โ 201 ImportUploadReceipt (ยง16);
400 over 100000 code points as sent, 413 over 100000 after NFC or 20000
non-blank lines, 415 for a NUL or invalid UTF-8, 409 IMPORT_CONFLICT
GET /teacher/imports/:id/sources/:sourceId/download โ a 60-second URL; 410 IMPORT_FILES_REMOVED
POST /teacher/imports/:id/process โ 202; 503 IMPORT_PROCESSING_UNAVAILABLE without a worker
POST /teacher/imports/:id/cancel
GET /teacher/imports/:id/review | PUT | POST /:id/review/adopt the draft, the teacher's save, a newer machine draft
GET /teacher/imports/:id/source?role= โ one source's extracted blocks
POST /teacher/imports/:id/commit โ one draft test
# teacher: AI assistant (ai.use; a suggestion only, nothing is saved; 501 AI_OFF without a provider)
POST /teacher/ai/questions/generate {source | testId + stimulusId, types (the five stored), count 3|5|8,
level b1|b2|c1, explanations, language}
โ {items: [{type, prompt, options?, answerKey, explanation}], usage}
POST /teacher/ai/tests/:id/check {checks โ key|clarity|giveaway|language, language?} โ {findings: [{severity,
questionId, number, type, title, issue, fix: {kind, description, label?,
payload?}}], usage}; 422 for a draft with no question or over 120,000 characters
POST /teacher/ai/tests/:id/variant {changes โ reword|reorder|distract, difficulty same|easier|harder} โ {changes:
[{questionId, number, kind, title, before, after, payload}], usage}; 422 as check
POST /teacher/ai/questions/:id/explanation {language, length?} โ {text, usage}; 422 without an answer key
POST /teacher/ai/questions/:id/distractors โ {items: [{text, why}] (up to 4), usage}; 422 unless a single or
multiple choice question with a key
POST /teacher/ai/attempts/:attemptId/answers/:questionId/score-suggestion
โ {score (0 to the points, steps of 0.5), confidence, reasons, comment, usage};
also needs teaching.grading and the caller's readStudentAnswers (403
AI_READ_OFF); 409 ATTEMPT_IN_PROGRESS | ATTEMPT_VOIDED; 422 for an answer the
grader marked itself or the student left empty
POST /teacher/ai/assignments/:id/insights โ {basis, items: [{kind, title, body, action, questionNumbers, types}]
(up to 4), usage}; the same gates; 422 when no paper is handed in
POST /teacher/ai/bank/cleanup {checks โ duplicates|tags, language?} โ {scanned, truncated, duplicates, tags,
usage} over the caller's own questions only, even for scope.all; 3 an hour
GET /teacher/ai/usage โ {month, used, allowance, left, paused, requests, pending, categories}
POST /teacher/ai/credit-requests {amount 1,000..100,000, reason} โ 201 AiCreditRequest; 409 AI_REQUESTS_OFF |
AI_REQUEST_PENDING; 5 an hour
# admin
DELETE /admin/users/:id a disabled student only, else 409 RESOURCE_NOT_ARCHIVED;
403 FORBIDDEN when the subset rule refuses,
409 RESOURCE_REFERENCED {referencedBy}
POST /admin/docs-session opens the API reference for fifteen minutes (ยง5.5)
# admin: AI credits (ai.credits.manage)
GET /admin/ai/credits โ {month, pool, daysToGo, onPace, withinPool, allowances, active, idle,
teachers, requests, rules}
PATCH /admin/ai/rules {defaultAllowance?, warnPercent? 50..99, onTeacherLimit? pause|request}
โ {rules, rewritten}
POST /admin/ai/credits/additions {credits} โ 201 AiPool; the approval to spend beyond the plan
PUT /admin/ai/teachers/:userId/allowance {custom?, credits?, paused?} โ AiTeacherMonth; 409
AI_ALLOWANCE_BELOW_USE; the subset rule applies (FORBIDDEN)
POST /admin/ai/credit-requests/:id/approve | /decline โ {request, allowance}; 409 AI_REQUEST_DECIDED |
AI_REQUEST_EXPIRED; the subset rule applies
# student
POST /app/classes/join {joinCode} โ Class (already-authed path)
GET /app/classes
GET /app/assignments โ {dueNow,upcoming,completed}; a card with a live attempt
carries liveAnsweredCount
GET /app/assignments/:id
POST /app/assignments/:id/attempts โ create or resume โ Attempt + ordered questions + sessionId
409 MAINTENANCE_SCHEDULED {startsAt, endsAt} for a start
that would run into a window
startOrResumeAttempt accepts resume; a Continue never
starts an attempt (#237)
GET /app/attempts/:id โ Attempt + questions + serverTime + audioPlays
a question's audio and a group recording's policy are
StudentAudioPolicy: maxPlays, allowSeek, showTranscriptAfterSubmit
and allowPause, all present
optional session names the tab's held session;
a different session on a live attempt answers
200 with superseded:true, that held session id,
and an empty beacon token, without a write
PATCH /app/attempts/:id/answers {sessionId,answers:[...],events:[...]} โ {savedAt, serverTime, deadlineAt}
POST /app/attempts/:id/events standalone flush (sendBeacon path)
POST /app/attempts/:id/audio-play {questionId,sessionId?} โ {plays, maxPlays}; 409 SESSION_SUPERSEDED | ATTEMPT_CLOSED | DEADLINE_PASSED
POST /app/attempts/:id/submit idempotent; 409 if already closed; 409 DEADLINE_NOT_REACHED
{deadlineAt} for timer_expired more than 5 s early
GET /app/attempts/:id/result โ Attempt + review policy + sections + questions,
each question naming its sectionId
GET /app/media/:assetId/url โ short-lived signed URL
Prefixes. /teacher/* holds the teaching operations, /admin/* the platform's, /app/* the student's and /auth/* the caller's own session and account; /me/* accepts only the self requirement. Every operation that requires a bearer token declares its permission as x-permission (ยง5). Under /teacher/* and /app/*, an id the caller does not reach answers exactly as a missing one does. The v0.7.0 paths under /admin/* that moved still answer until v0.9.1 (T-R3.3): the server rewrites each to its new path (DELETE /admin/students/:id becomes DELETE /admin/users/:id) and logs legacy_admin_path.
User adds only optional displayName and avatarUrl; it never carries
phone, locale, timeZone or preferences. Student class/intro/join-preview teacher
names use the chosen display name, falling back to full name, without a new
private field. CurrentUser.avatarUrl is the caller's photo as a presigned GET valid 24 hours, set by PUT /me/avatar and cleared by DELETE /me/avatar (T-R4.8); User leaves it unset until a list that shows other people's photos has its own task.
CurrentUser adds optional private phone, locale, timeZone and
preferences to User's fields, plus permissions, the keys the user's role holds in catalogue order, and workspaces, the consoles the user may open (teacher, admin, app). Only a response about the caller carries it: login, Google sign-in, GET and PATCH /auth/me, and POST /auth/google/link. A payload about someone else, such as the student on an attempt under review, carries User, so one user's permissions never reach another user's payload.
deleteClass and deleteUser answer RESOURCE_REFERENCED with details.referencedBy, which names what still references the row: assignments, attempts, audit, members, owned_content or other. Later releases add values, and a client treats one it does not know as other. The other permanent deletes (deleteAssignment, deleteTest, deleteTestVersion, deleteQuestionGroup) answer RESOURCE_REFERENCED without details.
During a maintenance window, every route answers 503 MAINTENANCE, with details {startsAt, endsAt}, Retry-After (seconds until the end) and a vi/en message. The exceptions are GET/HEAD /livez, /healthz and /public/status. The 503 comes before authentication and rate limiting, so an expired token also gets it. A POST /auth/logout refused this way still clears the refresh and docs cookies when it carried the refresh cookie. The family is not revoked: the device is signed out, but a copy of the token held elsewhere stays usable for as long as it is rotated, because each rotation renews the lifetime and the cleared cookie can no longer trigger reuse detection; only a password reset, a disable or a password change ends it. The same holds for a logout the limiter refuses (429) and one whose revoke fails (500).
A request the contract does not describe is answered in the envelope, in the caller's language and with the request id. A path nothing serves answers 404 NOT_FOUND, without the maintenance window being read. A path the router cleans (//auth/me, /a/../b) is first redirected to the cleaned path with 307, as before. A path under a method it does not serve answers 405 METHOD_NOT_ALLOWED with an Allow header naming the methods it does serve; during a window it answers the 503 as every request to that path does. HEAD is answered as GET without a body on the operations that need no token (GET /public/status), and on /livez and /healthz as before; on a GET that needs a token it answers the same 405, with or without a token. Neither answer reaches a handler or spends a rate-limit bucket.
Body readers and refusals. The rate limiter's body key reads first on
login, Google sign-in, join preview and in-app join; the body-limit middleware
reads next, and the contract validator reads the buffered body last. A
non-streamed body the limit middleware cannot finish reading answers
408 REQUEST_INCOMPLETE: the request may be sent again. The earlier body-key
reader can swallow a read error on those four operations, so a declared body
that ends early can still answer 400 instead of 408. Streamed uploads have
separate handling. Every operation taking a JSON body declares the
validator's 400 response; malformed or schema-invalid bodies are
400 VALIDATION_FAILED, and so is a request that carries the character U+0000 in
a body string, a body member name, a query parameter or a path segment (v0.83).
The AI assistant (D4). With no provider (ANTHROPIC_API_KEY unset, so Assembly.AI is nil) every /teacher/ai/* and /admin/ai/* operation answers 501 NOT_IMPLEMENTED with the code AI_OFF, before it reads anything, and PublicStatus.ai is false. Otherwise a suggestion operation, which needs ai.use, goes in this order: it reads what it will send through the caller's own scope (another teacher's id answers as a missing one), reserves its feature's ceiling in credits (generate 200, check 300, variant 300, explanation 20, wrong options 20, score 30, insights 60, bank clean-up 200), calls the provider, and ends the reservation with Finish, the one place that settles or releases. Finish settles what the provider billed, including the tokens of a refusal or of an answer that could not be used, and releases the rest, also when the handler panics or the caller goes away. The answer carries usage {credits, left}.
-
Refusals. A paused teacher is refused before any lock:
403 AI_PAUSED. An allowance or a pool that cannot cover the ceiling is402 AI_CREDITS_EXHAUSTEDwithdetails.scopeteacherorpool, and nothing is held.403 AI_READ_OFFis a teacher whosereadStudentAnswersis off asking for a feature that sends a student's answer. A provider that stays down after three retries, refuses, answers something no decoder can use, or rejects the request (a bad key or account, logged at ERROR for an operator) is503 AI_UNAVAILABLEwithRetry-After: 30, and nothing is written. The other codes areAI_REQUESTS_OFF,AI_REQUEST_PENDING,AI_REQUEST_DECIDED,AI_REQUEST_EXPIREDandAI_ALLOWANCE_BELOW_USE. -
The bank clean-up is one pass. It reserves its 200 credits once, before the first call, and plans only the calls whose worst case at the configured prices fits that reservation. It makes at most five calls at the same time (up to four of 50 questions to tag, and one that judges the closest pairs of prompts) inside 45 seconds, which with the 10 seconds allowed to settle keeps it under the proxy's 60-second idle timeout, and ends with one
Finishof the usage of every call the provider billed, on success, on a failed call, when the caller goes away and when a call panics. A call that fails, a call cut off by the time and a call the reservation could not cover leave what the other calls found in the answer withtruncatedtrue, and what was billed is settled; only a pass in which no call gave a usable answer is503 AI_UNAVAILABLE. A rewording that shares few words with the original is not found, and neither is a prompt typed without its diacritics next to its accented twin, because pairs are chosen by the words their prompts share, as typed; a duplicates call too long for its cap is cut to the best pairs that fit and the answer istruncated, never refused. - What a suggestion is checked against. A score is moved to the nearest multiple of 0.5 up to the question's points; a wrong option that repeats an option is dropped; a clean-up tag for a question the call did not ask about, and a duplicate pair the call did not offer, are dropped; a formatted question is read for its words and never rewritten.
-
What leaves the system (D20). Each prompt carries only what its feature needs. Generate: the pasted or chosen passage (its first 8,000 characters), the types, count, level and language. Check and variant: the draft's prompts, options, keys, explanations and passages, up to 120,000 characters; a check reads the members of the test's groups with their groups' passages, a variant only the standalone questions. Explanation and wrong options: the one question. Bank clean-up: the prompts, types and tags of the caller's own questions, newest first, at most 2,000 read, named q1 to qN in every call so that no id is sent; no call carries more than 50 questions to tag (the first 200 that have no tags, with the tags the bank already uses so a suggestion reuses them) or more than the questions of the 100 closest pairs of prompts, and a question without a pair is never sent for duplicates. Score: the question, its accepted or sample answers and the student's answer text. Insights: per question the correct rate and the eight most frequent wrong answer texts with their counts. No operation sends a student's name, email, id or class, an attempt id, or who wrote what;
operations_test.goandai_flow_test.goassert it with a fixture whose student is named in every other field. The two features that carry a student's words also needteaching.grading, and the score suggestion only for a short answer waiting for a mark by hand. No AI response reaches a student payload. -
The subset rule. The three Admin writes that name one teacher (
setAiAllowance, which also pauses and resumes, and the two credit-request decisions) callaccess.CanActOn; a target outside the subset answers403 FORBIDDEN.cmd/maintenance ai-creditsruns the same handlers as System and is not asked. -
Limits per signed-in user. The seven suggestions and the console writes 10 a minute and 100 an hour, the bank clean-up 3 an hour, the credit request 5 an hour, and the two reads (
getAiUsage,getAiCredits) 60 a minute and 600 an hour. -
Notifications. Three kinds join the nine, all for the teacher's bell and none with a switch:
ai.allowance_warning(once a month, when use crosses the warning share),ai.credit_request(to every holder ofai.credits.manage) andai.credit_request.decided. Their params areused,allowance,credits,teacherNameanddecision.
Notifications (R4). GET /me/notifications, POST /me/notifications/read, GET /me/summary and the five switches are the caller's own and need only self. A notification is a kind and its params; the server sends no sentence and the reader's console words it. Twelve kinds exist: for a teacher attempt.submitted, attempt.flagged, assignment.closing, class.joined, join_codes.rotated and the three AI kinds above, and for a student assignment.opened, assignment.due_soon, assignment.extended and result.ready. Five switches govern seven of them: attempt.submitted, attempt.flagged, assignment.closing and result.ready each their own kind, and assignment.due_soon the student kinds assignment.opened, assignment.due_soon and assignment.extended. class.joined, join_codes.rotated and the three AI kinds have none. A switch that is off writes nothing. A list is newest first, 20 by default and 50 at most, and is the only keyset list in the contract (before); every other list is offset-paginated (O-20). audience (v0.79) narrows a list, a count and a marking to one console's kinds.
Who is told, and when. A notification is written after the change that caused it has committed, under a budget of its own that the end of the request does not cancel; a failure is logged and never fails the request. The teachers are the assignment's creator and the teachers of a target class the student belongs to (visibility.PaperReaders, the rule that lets a teacher see a paper, read the other way), each once, so a teacher of another class is told nothing about this student. attempt.submitted merges the papers handed in on one assignment into one notification per fifteen minutes, a timed-out paper being a hand-in; attempt.flagged is written once, when the integrity policy flags a paper, and never for a teacher's own flag or under warn; class.joined, which has no switch, on a join by code; join_codes.rotated by the start-up rotation (ยง6.1). The student is told assignment.extended when notify is true on an extension (everyone whose close it moved) or on an override that moves a close (those it gave time past the assignment's own close), and result.ready when a teacher finishes grading and the review policy shows the score at that moment; a result.ready notification carries the title and the attempt, never a score. Due-time items. GET /me/summary and GET /me/notifications first write what the passing of time has earned the caller, at most once in five minutes per caller in the serving process; there is no timer and no job. A moment older than seven days is not made up, a row the caller already holds is left as it is, read or not, and a switch that is off writes nothing. For a student: assignment.opened (when the assignment opened or was published, whichever is later), assignment.due_soon a day and an hour before the student's own close (an override and an early close included) while they have handed nothing in and have an attempt left and the test was available at that moment, and result.ready at the close of an after_close release for a paper with nothing waiting for a mark. For a teacher: assignment.closing an hour before the assignment's close, with the number of students they reach who have not handed in and whose own close is the assignment's. A close that moves earns its reminders again.
The review options and the note. review.release is on_submit or after_close; review.showClassAverage and studentNote are the teacher's other two choices. On createAssignment an omitted field takes its default (on_submit, false, no note); on updateAssignment an omitted one keeps what is stored, every other field is replaced, and studentNote: null clears the note. The note is trimmed by the server and stored as NULL when nothing is left; the contract counts its 500 characters before trimming. It is plain text everywhere it is drawn and is carried to a student on the assignment's detail only.
The live lock. While the derived status is open (from opensAt up to, not including, closesAt, and before an early close), updateAssignment refuses a change to testVersionId, durationMinutes or maxAttempts with 409 ASSIGNMENT_LOCKED, whether or not anyone has started; sending the stored value is not a change. The check precedes the version check, so a scheduled or closed assignment that has attempts still answers VERSION_LOCKED for a changed version, and its timing stays editable.
The closed rule. An assignment that is closed (its window has ended, or it was closed early) stays closed under updateAssignment. A window that would make it scheduled or open again, or that opens after the early close it keeps, answers 409 ASSIGNMENT_CLOSED: that is a reopening, and reopenAssignment does it with a reason and an audit row. The two operations take complementary sets, as extendAssignment and reopenAssignment do. A save that leaves it closed is unaffected: the stored window, an earlier close, closeNow (while the window has opened: with opensAt ahead of now it is 400 VALIDATION_FAILED naming closeNow, for every status), any other field, and a later close on an assignment closed early (a save never lifts an early close).
Release after the close. The effective close of an assignment is the earlier of its early close and closesAt. Until now reaches it, a result whose assignment releases after_close is read as if the policy hid the score, the marks, the answer key and the explanations, and its grader's comments are not sent: the result's review.showScore, showCorrectAnswers and showExplanations are false, review.release and review.showClassAverage are as stored, and releasesAt is that close. A pending manual answer and a transcript keep their own gates. The student's assignment card shows no score before the same moment.
classAverage on a result is the mean, over the students who qualify, of each one's best graded attempt as earned over total, as a percent (0 to 100, rounded to two decimals). A student qualifies when they are targeted (through a class or by name) and enabled and have a graded attempt; voided, submitted-but-ungraded and timed-out-but-ungraded attempts count for nothing. It is present only when review.showClassAverage is on, this result is released, the assignment has closed and at least 3 students qualify, and no student is named.
liveAnsweredCount on a student's assignment card is the number of the live attempt's saved answers that say something, by the rule the engine's navigator applies (web/src/features/take-test/answered.ts): a choice with an option picked, a true/false with a value, a text that is not blank, and a fill-in with every blank of the frozen question filled. Blank means empty after removing the whitespace JavaScript's trim() removes. An answer that exists only in the browser's draft is not counted. The field is absent when there is no live attempt. The teacher's monitor applies the same rule to a row's answeredCount, so a saved answer the student has since cleared counts on neither screen.
classIds on a student's assignment card is every class the assignment targets that contains the student and is not archived, empty for a student targeted only by name, so a paper given to two of the student's classes can be next on both class cards; classId and className are set only when it holds exactly one class.
The result's sections are the paper's parts in test order, and every result question names its sectionId. Both are present under every review policy, because the attempt already showed the student its parts; the page sums a part's score from its questions' earned, which the policy still gates.
The teacher's lists return { items, page, pageSize, total } with the list's own facets, and are offset-paginated by page and limit so a client can draw numbered pages (O-20 overrides ยง13.8's keyset rule for them); GET /me/notifications returns { items, nextBefore } (keyset). Student payloads never include isCorrect, sampleAnswer, acceptedAnswers, or transcript (the last only per showTranscriptAfterSubmit, on the result endpoint).
Section of Quizzivy โ Frontend Portal & Data Model Specification ยท Previous ยท Next
Generated from docs/quizzivy-spec-v0.3.md. Edit that file in the repository; changes made on this page are overwritten.