OneRoster gradebook + resource services — design brief (v1)
Status: implemented (2026-06-12) — Anton settled the three gating decisions
the same day (gradebook_entry + grade_category in core; resources
boundary-only; implement immediately) and the proposed plan below landed as
written. The gradebook write operations (PUT/POST/DELETE in the binding),
originally deferred here as read-only-in-v1, subsequently landed in the
deferred-items campaign (2026-06-12, slice B — provider-write-service.ts,
core-backed PUT/DELETE × {categories, lineItems, results, scoreScales} under
the gradebook.createput/gradebook.delete scopes; see
oneroster-provider-v1.md → Deferred-items campaign). Resources stay
boundary-only (snapshots; no core projection) until a real SIS resource feed
exists.
Builds on: oneroster-provider-v1.md (rostering provider + REST consumer),
ADR-0012 (projection, not roster core), ADR-0014 (erasure)
Contracts: @conform-ed/contracts oneroster/v1_2 gradebook + resource
service schemas (already shipped — wire shapes are settled)
Both services ARE OneRoster 1.2 (separate service specs sharing the rostering base). They were not implemented with the rostering milestone because each requires a core-model decision the rostering work did not: the one-vocabulary rule means a neutral core home must exist before a boundary can project into it (the Learning Period precedent), and these homes don’t fully exist yet.
Gradebook services — gap analysis
Section titled “Gradebook services — gap analysis”Wire entities: Category, ScoreScale, LineItem, Result (+ AssessmentLineItem / AssessmentResult, the class-independent variants).
What the core already has (assessment-feedback.ts):
| wire | core | fit |
|---|---|---|
| LineItem.class/school/title | assessment_definition (offering-scoped, title) |
clean |
| LineItem.resultValueMax | assessment_definition.max_score |
clean |
| Result.score/scoreDate/student | assessment_score (raw/scaled, is_final) + attempt person/finalized_at |
provider direction mostly clean |
| Result.comment | feedback_entry |
clean enough |
The four real gaps:
- Results are attempt-less; core scores are attempt-rooted.
assessment_scorehangs offassessment_attempt(NOT NULL FK, attempt_no, started/submitted timestamps). A SIS-imported grade has no attempt — there is nothing true to put in those columns. Consuming results into the core therefore needs either a new neutral entity (a gradebook entry keyed by definition + person, whereassessment_scorerows project INTO it as the platform-native evidence) or synthetic attempts (false data by construction — rejected on principle). - LineItem REQUIRES
assignDate+dueDate.assessment_definitionhas neither. The provider cannot emit conformant lineItems without a due-date home (deriving from offering window is semantically wrong per item). Due dates are also a real product need (teacher gradebook UI), so this is a core column decision, not a boundary hack. - LineItem REQUIRES a
categoryref. The core has no grade-category entity (gradebook weighting groups). Synthesizing one category per class is conformant-but-hollow for the provider; the consumer would have no home for real SIS categories. Categories are also what the spreadsheet-like gradebook UI needs. - ScoreScale has no core analog (letter-grade scales). Optional on the wire — v1 can keep scales boundary-only (snapshots) and emit numeric scores only.
Implied sequencing: a short core design session (gradebook entry, category, due/assign dates — decisions Anton must validate per the working rules), THEN the boundary work, which is mechanical from the rostering patterns: snapshot + mapping tables, reconcile sections, projection + routes (the query engine, auth, paging, and round-trip-gate machinery all already exist and are reused as-is).
Resource services — gap analysis
Section titled “Resource services — gap analysis”Wire: Resource (vendorResourceId REQUIRED, title/roles/importance optional) + class/course association files — vendor content POINTERS, not content.
Core analog: external_package + assignments hold actual imported content;
there is no “vendor resource pointer” entity. Options: (a) boundary-only in
v1 (snapshots + mappings reserved, no core projection — the demographics
treatment) — consistent with ADR-0012 and zero core cost; (b) project onto
external-package assignments where vendorResourceId matches an imported
package — speculative until a real SIS resource feed exists. No current
product pull either way.
SIS-id echo (recorded here because it’s gradebook-coupled)
Section titled “SIS-id echo (recorded here because it’s gradebook-coupled)”Not a OneRoster spec feature. It matters only for passback INTO a SIS that keys on its own sourcedIds (results referencing SIS lineItem/student ids). The core-uuid decision stands for everything we provide; if a passback integration requires echoing, the consumer mapping tables already hold the correlation and the provider can translate at the boundary. Decide when a concrete passback target exists — deciding now would be designing against an imagined consumer.
Proposed plan (pending the gating decisions)
Section titled “Proposed plan (pending the gating decisions)”- Core:
grade_category(org/offering-scoped, title, weight),gradebook_entry(definition + person, score numeric + status enum + recorded_at + meta; erasure: sever person),assessment_definitiongainsassign_at/due_at(nullable) +grade_category_id(nullable).assessment_scorestays the attempt-rooted evidence; finalized scores project intogradebook_entry(platform-native rows carry provenance). - Boundary:
oneroster_line_item_snapshot/_mapping(→ assessment_definition),oneroster_category_snapshot/_mapping(→ grade_category),oneroster_result_snapshot(→ gradebook_entry via lineItem mapping + person binding), scoreScale snapshots (boundary-only). - Reconcile order extends: categories → lineItems → results, same bulk/delta semantics, same held-user behavior (results for held users stay pending in snapshots).
- Provider: projection gains categories/lineItems/results; gradebook routes from the binding table; CSV files added to the manifest; round-trip gate extends with a graded fixture.