| Django model/service |
Create/edit/duplicate legacy and modules; omit/invalid format; two sibling Cohorts with different formats |
Explicit value required at boundaries, deterministic default only for migration, sibling isolation, revision/audit behavior |
| Django model/service |
Module/Unit/order/terminal Homework and Module/Project flow constraints |
Same-Cohort ownership, unique positions/slugs, XOR target, exactly-once published items, one-to-one published Module/terminal-Homework coverage, no published flow on legacy, transaction rollback on invalid writes |
| Django rendering/contract |
Existing release-contract fixtures as legacy |
Same Homework/Projects headings, table order, status/deadline links, HTML contract, metadata/cache; no module markup/data mutation |
| Django rendering/contract |
Modules A/B, Units A1/A2/B1, Homework HA/HB, Projects X/Y with flow A/X/B/Y |
Exact flow and unit order, HA/HB terminal, X/Y in flow, no separate Projects section, stable query count/no cross-Cohort leakage |
| Django criteria |
A=C1,C2, B=C2,C3, zero-criteria project, reordered assignments |
Exact ordered form/results/export sets, actual reuse of C2, no cohort-wide fallback, deterministic empty behavior |
| Django criteria/security |
POST foreign/unassigned/deleted/duplicate criterion; wrong Cohort/Project/review/reviewer |
Expected 400/403/404 contract, atomic zero writes, exact object-graph binding, no existence leak |
| Django project lifecycle |
At least number_of_peers_to_evaluate + 1 synthetic enrolled submitters in each format |
First submission, no self-review, configured assignment count, review states/responses, close/score/results, identical expected score/vote/badge/leaderboard behavior |
| Django adapter characterization |
Existing project assignment, review, scoring, result, reminder/job and export suites |
Existing algorithm outputs and after-commit side effects unchanged; only project-criteria source differs |
| Django/API/Studio |
Read/write/reorder/duplicate module flow and shared/distinct criteria with stale revision/replayed idempotency key |
Scoped permissions, deterministic serialization/order, conflict handling, audit events, no partial graph |
| Django URL/SEO/cache |
Canonical and every retained legacy path for both formats, slash/case/query variants, reverse calls |
#53 route ownership, correct serve/redirect/status, canonical/noindex, private no-store, no format-specific URL fork |
Data migration (MigrationExecutor) |
Pre-feature legacy DB -> expand/backfill -> contract; rerun; migrate back before module data; fresh install |
All Cohorts legacy except explicit mapping, stable PKs/counts/checksums/order, idempotency, reversible additive step |
Data migration (MigrationExecutor) |
Shared/distinct post-migration graph; orphan criteria; mismatched response/score; colliding/cross-Cohort flow; reverse with module rows |
All-project legacy criterion links, preserve/report orphan, block/quarantine mismatch, reject invalid flow, refuse destructive reverse, no silent loss |
| Production-like rehearsal [HUMAN] |
Authorized redacted snapshot with representative large/complex Cohorts |
Preflight/rejected counts, duration, key/checksum and sampled score parity, allow-list review, rollout/rollback gate; no PII in artifacts |
| Playwright legacy |
Existing legacy Cohort at desktop/mobile and JavaScript disabled |
Visual/interaction parity, two sections/tables, statuses/deadlines/actions reachable, no module UI, no horizontal page overflow |
| Playwright module |
A/X/B/Y flow at desktop/mobile, disclosures expanded/collapsed and JavaScript disabled |
Exact order, terminal Homework, in-flow projects, persistent status/actions, fallback content, refresh/deep-link stability |
| Playwright mixed/cache |
Navigate Course -> legacy Cohort -> module Cohort -> back; two users with different access |
Format/criteria never leak across navigation/session/cache; canonical links and safe authorization hold |
| Playwright lifecycle |
Synthetic users complete submission -> assigned reviews -> scoring/results from an in-flow Project |
Existing destinations/states/results work from module flow and legacy page; screenshots contain synthetic data only |
| Playwright accessibility + manual |
Legacy and module pages at adopted viewports, keyboard, named screen reader, 200%/400% zoom |
Axe has no new serious/critical violations; semantic order/name/state/focus/reflow/touch/contrast requirements pass |
Parent epic: #5
Normative references:
Related decisions and delivery issues: #14, #15, #16, #51, #53, #55, #56, #60.
Problem statement
The basic Course -> Cohort phase deliberately preserves today's cohort-owned Homework, Project, submission, peer-review, and scoring behavior. A follow-up is required because cohorts need two curriculum presentations:
legacy, preserving the existing Homework table followed by the separate Projects table; andmodules, where a cohort has ordered modules and units, every module ends with homework, and projects occupy configured positions in the same ordered learning flow instead of a separate Projects section.The format choice belongs to each Cohort. Most migrated cohorts must remain legacy without data or rendering drift, while selected existing cohorts and new cohorts may opt into modules. Legacy and module cohorts must coexist, including under one Course.
The current/adopted criteria ownership is also too broad: criteria are looked up for the whole course/cohort. Criteria must instead be assigned to projects in an explicit order and be reusable, so projects in one cohort can share some criterion definitions while retaining different rubrics. This must be an adapter-level change around the existing submission -> peer review -> scoring/results lifecycle, not a rewrite of that lifecycle.
Scope
Explicit non-goals
Dependencies and sequencing
_docs/specs/04-courses-and-cohorts.mdcurrently place ReviewCriteria on Cohort. They must be amended/accepted to describe independent criterion definitions plus ordered project assignments before engineering starts.Data/model ownership and invariants
Names below are logical contracts; implementation names may differ while preserving these invariants.
Cohort format and curriculum
curriculum_formatis required on Cohort, never inherited implicitly from Course. Its supported values are exactlylegacyandmodulesfor this issue.legacy. The migration takes an explicit reviewed allow-list/mapping for any selected existing Cohort converted tomodules; heuristic inference from titles, slugs, existing assignments, or dates is forbidden.legacy. Existing Homework, Project, criterion, submission, review, and score rows are not rewritten to simulate modules.Project criteria
criteria_for_project(project)-equivalent boundary. No consumer may fall back to “all criteria in the Cohort.”CriteriaResponseandProjectEvaluationScoreretain their existing primary keys and criterion references. The adapter must not recalculate historical outcomes or change assignment/scoring/review algorithms.Migration and rollback constraints
legacyexcept identifiers in the reviewed module allow-list. Re-running the backfill is idempotent and produces the same result.Browser, URL, API, accessibility, and security behavior
curriculum_format, deterministic module/unit/flow order, and ordered project criteria without returning another Cohort's objects. Writes use the revision/idempotency/audit conventions from Add Course/Cohort lifecycle, public routes, and complete cohort duplication #53.no-storeand non-indexable. Anonymous, unenrolled, wrong-Cohort, wrong-Project, and wrong-review users receive the existing safe denial/404 behavior with no object-existence leak and no writes.Acceptance criteria
Engineer-owned implementation evidence
legacy|modulesformat; Course has no curriculum-format switch and changing one Cohort does not change any sibling.legacyon every run; only a reviewed explicit identifier mapping becomesmodules; source Homework/Project/criterion/history values and PKs are unchanged and migration evidence records counts/checksums.legacyCohort.Module A -> Project X -> Module B -> Project Yrenders exactly in that order; Units retain Module order, each Module ends with its Homework, project lifecycle actions remain usable in place, and no separate Projects section appears.[C1,C2]and Project B=[C2,C3], forms/APIs/exports/results use those exact ordered sets, C2 is one reusable definition, and neither project receives the other's unassigned criterion.no-store/non-indexable, automated accessibility checks pass, and the manual checks below have evidence.make test-playwright-core, both format-specific browser suites, static/type/migration checks, and route/release contracts pass with no unrelated baseline regression.Independent tester evidence
.tmp/screenshots/...evidence with no secrets or personal/production data.Product-manager acceptance evidence
Required test matrix
Suggested test locations may change, but every row is required and must map to evidence.
legacyandmodules; omit/invalid format; two sibling Cohorts with different formatsC1,C2, B=C2,C3, zero-criteria project, reordered assignmentsnumber_of_peers_to_evaluate + 1synthetic enrolled submitters in each formatno-store, no format-specific URL forkMigrationExecutor)MigrationExecutor)Minimum engineer commands/evidence include focused
uv run --frozen python manage.py test ...suites for the rows above,uv run --frozen python manage.py check,uv run --frozen python manage.py makemigrations --check --dry-run, the repository verification-plan/run targets, andmake test-playwright-core. The independent tester reruns rather than reuses engineer output.Product decisions required before engineering
Verification handoff
.tmp/screenshots/...paths for desktop/mobile/manual checks.