Skip to content

Data Model

The @repo-edu/domain package is where every data structure in the system is defined. It contains no I/O, no side effects, and no Node or Electron imports, which means it runs identically in the Electron desktop app, the CLI, and the browser-based docs demo.

Domain types — the TypeScript interfaces that describe settings, courses, rosters, and every other entity — live in packages/domain/src/types.ts. Companion Zod schemas in packages/domain/src/schemas.ts validate data at boundaries: the points where the application reads or writes JSON files. When a persisted file is loaded from disk, the schema checks that its shape matches what the code expects. If it doesn’t, the load fails with structured, path-level error messages rather than producing subtle runtime bugs.

Each persisted document carries a kind discriminator and a schemaVersion field:

DocumentKindCurrent version
App settingsrepo-edu.app-settings.v11
Courserepo-edu.course.v11

These markers exist for future schema evolution. There is no migration layer — invalid documents are rejected at the boundary.

PersistedAppSettings stores application-wide state in a single file.

FieldTypeDescription
activeCourseIdstring | nullCurrently selected course
activeTab"roster" | "groups-assignments"Last active UI tab
appearanceAppAppearanceTheme, window chrome, date/time format
windowPersistedWindowStateWindow width and height (default 1180×760)
lmsConnectionsPersistedLmsConnection[]Canvas/Moodle connections (name, provider, baseUrl, token)
gitConnectionsPersistedGitConnection[]GitHub/GitLab/Gitea connections (id, provider, baseUrl, token)
lastOpenedAtstring | nullISO timestamp of last app open
rosterColumnVisibilityRecord<string, boolean>Per-column visibility state for roster table
rosterColumnSizingRecord<string, number>Per-column width for roster table
groupsColumnVisibilityRecord<string, boolean>Per-column visibility for groups table
groupsColumnSizingRecord<string, number>Per-column width for groups table

AppAppearance contains theme ("system", "light", "dark"), windowChrome ("system", "hiddenInset"), dateFormat ("MDY", "DMY"), and timeFormat ("12h", "24h").

PersistedCourse stores all data for a single course.

FieldTypeDescription
idstringUnique course identifier
displayNamestringHuman-readable name
revisionnumberMonotonically increasing save counter for compare-and-swap writes
lmsConnectionNamestring | nullReferences a connection in app settings by name
gitConnectionIdstring | nullReferences a Git connection in app settings by id
organizationstring | nullGit organization/group for repository operations
lmsCourseIdstring | nullLMS-side course identifier
rosterRosterStudents, staff, groups, group sets, assignments
repositoryTemplateRepositoryTemplate | nullDefault template for repo creation
repositoryCloneTargetDirectorystring | nullLocal directory for clone operations
repositoryCloneDirectoryLayout"flat" | "by-team" | "by-task" | nullHow to organize cloned repos
updatedAtstringISO timestamp of last save

The revision field enables compare-and-swap: the save workflow rejects writes where the supplied revision doesn’t match the stored one, preventing lost updates from concurrent editors.

A Roster contains the full student/staff/group/assignment graph for a course.

Each roster member (student or staff) has:

FieldTypeDescription
idstringUnique within the roster
namestringDisplay name
emailstringPrimary email
studentNumberstring | nullInstitution-specific ID
gitUsernamestring | nullGit provider username
gitUsernameStatus"unknown" | "valid" | "invalid"Verification status against Git provider
status"active" | "incomplete" | "dropped"Current enrollment status
lmsStatusMemberStatus | nullStatus from LMS (may differ from local status)
lmsUserIdstring | nullLMS-side user ID for sync
enrollmentTypeEnrollmentType"student", "teacher", "ta", "designer", "observer", "other"
sourcestringOrigin of this member record

Tracks how the roster was populated — a discriminated union on kind:

  • "canvas" / "moodle" — imported from LMS, carries courseId and lastUpdated
  • "import" — imported from file, carries sourceFilename and lastUpdated

A Group is a named collection of member IDs with an origin ("system", "lms", "local"). Two system group sets are always present: individual_students and staff.

A GroupSet organizes groups for assignment purposes. It has a connection (discriminated union: "system", "canvas", "moodle", "import") and a groupSelection that controls which groups are active — either "all" with optional exclusions, or "pattern" with a filter string.

An Assignment links a groupSetId to an optional RepositoryTemplate. The template is a discriminated union:

  • "remote"owner + name on the Git provider, with visibility
  • "local" — local file path, with visibility

Two functions validate persisted documents at load boundaries:

  • validatePersistedAppSettings(value)ValidationResult<PersistedAppSettings>
  • validatePersistedCourse(value)ValidationResult<PersistedCourse>

Both use Zod schemas under the hood. On failure, they return { ok: false, issues } where each ValidationIssue has a dot-path ("roster.students.0.email") and a message. Invalid files are rejected — there is no partial-load or best-effort parsing.

Compile-time drift guards in schemas.ts ensure the Zod inferred types stay in sync with the hand-authored TypeScript types.

Beyond schema validation, @repo-edu/domain performs semantic roster validation via the validation.roster workflow. This catches 17 kinds of issues:

KindWhat it catches
duplicate_student_idTwo students with the same ID
missing_email / invalid_email / duplicate_emailEmail problems
duplicate_assignment_nameNon-unique assignment names
duplicate_group_id_in_assignment / duplicate_group_name_in_assignmentGroup uniqueness within assignments
duplicate_repo_name_in_assignmentRepository name collisions
orphan_group_memberGroup references a member ID that doesn’t exist
empty_groupGroup with no members
system_group_sets_missingRequired system group sets not present
invalid_enrollment_partitionMember in wrong collection (student in staff or vice versa)
invalid_group_originGroup origin inconsistent with its group set connection
missing_git_username / invalid_git_usernameGit username problems for active members
unassigned_studentStudent not in any group for an assignment
student_in_multiple_groups_in_assignmentStudent assigned to multiple groups

Each RosterValidationIssue includes affectedIds (member/group IDs) and optional context for diagnostic messages.