Grades surface iteration 3 — authoring scales, assessment titles, and grade types
1. Problem distillation
- The subject-group gradebook exposes criterion ids and names but not the
effective ordered values that a writer may submit, forcing the frontend to
guess and receive
GRADE_VALUE_NOT_ON_SCALE after submission.
- Iteration 2 validates new grades against live effective scales but validates
corrections against the revision's frozen scales. The response contract must
expose both contexts in the surfaces where each is used.
- Approved frontend prototypes distinguish an assessment's short display title
from its student-facing note, but the immutable grade revision has no title.
- The fixed assessment taxonomy needs
ASSIGNMENT and PROJECT in addition to
WRITTEN, ORAL, and PRACTICAL.
Success criteria (observable behavior that proves this works):
GET /grades/subject-group/:id returns the effective subject and criteria
scale values in display order for every authorable leaf/unit context.
- A leaf gradebook returns both scales at the root. A container returns its
common subject scale at the root and both effective scales inside every unit;
the root criteria scale is null only when
criteriaLevel = UNIT, where no
truthful single container criteria scale exists.
- Every
GradeEntryResponseDto returns the revision's frozen subject and
criteria scales. They are non-null for COMPLETE revisions and nullable only
for honest LEGACY_INCOMPLETE revisions.
RecordGradeDto accepts an optional trimmed title of 1–120 characters.
UpdateGradeDto preserves it when omitted, replaces it with a string, and
clears it when explicitly null.
- Every new revision, including corrections and withdrawals, carries the full
post-command
title snapshot. Existing revisions remain null without a
fabricated backfill.
GradeEntryResponseDto exposes title: string | null on create, correction,
withdrawal, gradebook, student, and history responses.
GradeType accepts and returns ASSIGNMENT and PROJECT without changing
the semantics of the existing three values.
- The teacher exclusion from
GET /grades/homeroom/:id and all parametric
department/curriculum visibility rules remain unchanged.
Non-goals (in-scope-shaped things this iteration is explicitly not doing):
- No configurable assessment-type catalogue, custom types, type weights, or
per-type aggregate rules.
- No API-level requirement that every new grade have a title; the frontend may
require it, while the backend stays additive for deployed iteration-2 clients.
- No title on the mutable legacy
GradeEntry compatibility columns; the
revision ledger remains the sole grade-state authority.
- No
/permissions representation of route-level @RequireRoles metadata and
no role admission or visibility changes.
- No homeroom-level aggregate legend field. Student and history surfaces receive
frozen scales through their nested grade-entry responses.
- No implementation of an unspecified fifth frontend item; the supplied brief
contained four numbered items.
2. Patterns survey
| Analogous module/spec |
What we'd borrow |
What doesn't fit |
2026-08-12-grades-surface-iteration-2-design.md |
Stable root plus immutable full-state revisions, frozen scale evidence, revision-native response mapping, and parametric visibility |
Its explicit deferral of assessment titles and new grade types is overturned only for the two confirmed additions in this iteration. |
src/grades/grades.scale-context.ts |
The single authority for resolving effective live subject/criteria scales and ordered values for leaf, shared-criteria container, and unit-criteria contexts |
Live scale context is suitable for new authoring only; it must not replace frozen evidence when editing or displaying an existing revision. |
src/grades/grades.scale-snapshot.ts |
Versioned immutable scale snapshots and ordered values, decoded without live evaluation-scale rows |
Public DTOs expose only { id, name, values: { value, label }[] }; numeric calculation evidence remains internal. |
src/evaluation-scales/dto/evaluation-scale-response.dto.ts and evaluation-scales.queries.ts |
Swagger-declared scale/value DTOs and database ordering by ordinalPosition |
The grade surface must expose the resolved scale, not the whole tenant/platform catalogue or administrative fields such as inUse. |
src/grades/grades.service.ts |
revisionSnapshotData carries unchanged fields into correction/withdrawal revisions, while explicit DTO fields replace only when present |
title: null needs an explicit clear branch rather than the current string-only optional-field typing. |
This iteration is on-axis under docs/16-maintainability.md §4–§5. It adds
one field to the already-established grade-revision snapshot and extends an
existing enum/aggregate DTO. It introduces no new entity, RBAC primitive,
cross-module write, or cross-cutting concern.
3. Architecture mapping
| Primitive |
Apply? |
How |
Justify |
| Tenant scope |
yes |
Existing GradeEntryRevision.tenantId, tenant-bounded root/revision queries, and forced RLS remain unchanged. |
Title and enum values are fields of existing tenant-owned revisions. |
| Academic-year scope |
yes |
Existing active-year create and explicit historical read/correction rules remain unchanged. |
No new time-scoped record is introduced. |
| RBAC entity key |
existing grade_entries |
No entity-key delta. |
Scale/title/type are part of the existing protected grade record. |
| Scopes |
existing record plus aggregate read |
No catalogue or field-map delta; grade endpoints remain aggregate responses. |
The module does not use scope-grouped response fields. |
| Actions |
existing record |
No action delta. |
Authoring authority is unchanged. |
| Service base |
custom GradesService |
Extend existing gradebook assembly and revision snapshot construction. |
Immutable ledger commands do not fit BaseTenantedCrudService. |
queries.ts shape |
existing include/select constants + named helpers |
Reuse gradeRevisionInclude, toGradeEntryDto, and loadSubjectGroupForGrade; reuse loadScaleContext as the live resolver. |
No repository or inline parallel pattern is needed. |
| Error codes |
existing |
Reuse GRADE_SUBJECT_NOT_GRADABLE, GRADE_VALUE_NOT_ON_SCALE, and existing validation responses. |
The new fields introduce no new domain failure. |
| DTO conventions |
aggregate DTOs |
Add a small grade-owned scale projection DTO and compose it into gradebook/entry DTOs. Extend write DTOs with documented title semantics. |
Administrative scale DTOs contain fields the grade client neither needs nor should infer from. |
| File-backed sub-resources |
n/a |
No files. |
Not part of grade authoring metadata. |
| Custom fields |
no |
n/a — fixed contract fields only. |
Title and type are governed academic metadata. |
| Profile completeness |
no |
No required-profile-field delta. |
Grade authoring is not profile completion. |
4. Data model plan
Schema deltas
- Add nullable
title String? @db.VarChar(120) to
GradeEntryRevision; do not add it to the legacy root.
- Append
ASSIGNMENT and PROJECT to Prisma/PostgreSQL GradeType.
Migration shape
- Additive / destructive / renaming: additive only — one nullable column and
two PostgreSQL enum labels.
- Data backfill: none. Existing revision titles remain null because no reliable
assessment name exists to reconstruct.
- Hazards from chapter 12 checklist: PostgreSQL enum labels are intentionally
append-only and cannot be safely removed/reordered in a rollback; the new
nullable no-default column requires no table rewrite or fabricated data; the
migration performs no enum-value DML in the same transaction.
Indexes and uniqueness
- None. Title is display metadata, not an identity or search key in this
iteration; grade-type cardinality does not require an index.
5. API surface
| Verb |
Path |
Decorators |
Request DTO |
Response DTO |
| POST |
/grades |
existing grade_entries.record:write + record action |
RecordGradeDto gains optional title; type accepts five enum values |
GradeEntryResponseDto gains nullable title plus frozen scales |
| PATCH |
/grades/:id |
existing grade_entries.record:write + record action |
UpdateGradeDto gains replace/preserve/clear title semantics; type accepts five enum values |
Same extended GradeEntryResponseDto |
| POST |
/grades/:id/withdraw |
unchanged write scope/action |
unchanged |
Same extended GradeEntryResponseDto, carrying the withdrawn revision's title/scales |
| GET |
/grades/subject-group/:id |
unchanged read scope and role admission |
unchanged temporal/range query |
SubjectGroupGradebookDto and every GradebookUnitDto gain resolved scale context; nested entries gain frozen scales/title |
| GET |
/grades/student/:id |
unchanged |
unchanged |
Nested grade entries gain frozen scales/title |
| GET |
/grades/me |
unchanged |
unchanged |
Nested grade entries gain frozen scales/title |
| GET |
/grades/:id/history |
unchanged |
unchanged |
Selected/history revisions gain frozen scales/title |
Swagger considerations
- Document scale arrays as display-ordered; do not expose
ordinalPosition
because array order is the public ordering contract.
- Document frozen response scales as nullable only for
LEGACY_INCOMPLETE revisions.
- Document the container root
criteriaScale null case explicitly and direct
clients to units[*].criteriaScale when isContainer uses unit criteria.
- Document
title create compatibility and PATCH omission/string/null
semantics. Advertise all five GradeType values through the generated enum.
6. RBAC seed plan
| Seed file |
Delta |
PermissionScope (rbac-catalogue.ts) |
none |
PermissionAction (rbac-catalogue.ts) |
none |
ScopeFieldMapping (rbac-catalogue.ts) |
none — grades use aggregate DTOs |
Role grants (roles.ts) |
none |
*_SCOPES runtime constant |
none |
7. Divergence ledger
| Pattern |
We diverge by |
Reason |
Tradeoff accepted |
| Iteration-2 deferrals |
Add assessment titles and two fixed grade types now. |
Product confirmed the approved frontend prototypes require them. |
Small additive migration and wider contract surface. |
| FE-requested non-null root scale pair |
Root criteriaScale is nullable for a UNIT-criteria container. |
Such a container has one criteria scale per unit; returning any single scale would be false. |
FE branches on the already-present container/unit shape. |
| Live scale only in gradebook |
Also expose each revision's frozen scales. |
A correction must use the old revision scale even after configuration changes. |
Some response duplication in exchange for deterministic edits and legends. |
Legacy compatibility columns on GradeEntry |
Store title only on GradeEntryRevision. |
New application state is revision-native and the root columns are scheduled for later removal. |
Legacy SQL consumers do not receive title; none are authoritative. |
8. Pushback log
| US says |
Conflicts with |
Proposed instead |
Status |
Return non-null subjectScale and criteriaScale at both root and unit levels. |
criteriaLevel = UNIT has no single effective container criteria scale. |
Root criteriaScale = null in that case; every unit always returns its effective pair. |
Resolved — correctness takes precedence and FE fields were prepared as optional. |
| Assessment name is mandatory in prototypes. |
Existing iteration-2 clients cannot provide it and historical revisions cannot be backfilled honestly. |
Optional backend input/output, with the FE requiring it for new interactive authoring. |
Resolved — additive rollout. |
/permissions cannot predict @RequireRoles failures. |
Solving it changes the cross-cutting RBAC capability contract, not the grade entity. |
Keep current teacher exclusion and parametric visibility; design route-capability discovery separately if prioritized. |
Resolved — deferred. |
| The brief announces five remaining things. |
Only four numbered items were supplied. |
Implement only the four received/confirmed items. |
Resolved — no fifth requirement inferred. |
9. Deferrals
- Route-level capability discovery in
/permissions — cross-cutting RBAC design
beyond grades — follow-up: separate design iteration if prioritized.
- Configurable assessment types and weighting — five fixed types are sufficient
for the confirmed prototypes — follow-up: future evaluations/report-card spec.
- Backend-required titles — deferred to avoid breaking deployed iteration-2
clients; reconsider only with a versioned contract/cutover.
- Homeroom aggregate scale legend — not blocking and aggregate display scale
needs an explicit course-level contract — follow-up: next read-surface iteration.
- Any fifth frontend gap not present in the supplied brief — follow-up when the
concrete requirement is provided.
10. Open questions
- None. Product confirmed
title, ASSIGNMENT, and PROJECT on 2026-08-13;
scale nullability and current-vs-frozen placement are resolved above from the
existing unit and immutable-revision invariants.
11. Verification plan
- Unit specs:
src/grades/grades.scale-context.spec.ts: live values remain ordered and
leaf/shared/unit resolution returns the expected scale pair.
src/grades/grades.service.spec.ts: leaf and container gradebooks expose
the specified scale placement; record/update/clear/withdraw preserve the
correct title; response mapping returns frozen scales; both new grade types
pass through the ledger.
src/grades/grades.scale-snapshot.spec.ts: public scale projection keeps
snapshot array order and removes internal numeric evidence.
- E2E specs:
test/grade-entries.e2e-spec.ts: create ASSIGNMENT/PROJECT grades with
title, replace and clear title through revisioned PATCH, read title/scales
in gradebook/student/history, and verify unit-specific scale values/order.
- Existing role scenarios continue to prove teachers receive no homeroom
route access and curriculum coordinators remain parameter-bounded.
- Manual verification: inspect generated OpenAPI for five enum values,
nullable title semantics, ordered compact scale DTOs, and container root/unit
scale descriptions.
12. Sign-off
- Approved by: Fabio
- Date: 2026-08-13
- Chat reference: approved in chat on 2026-08-13 after the iteration-3 contract walkthrough
Until this section is filled, no implementation code is written. When it is
approved, flip frontmatter status: to Approved in the same edit and commit
this spec before implementation.