Skip to content

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.