Skip to content

Support legacy and module Cohort curricula with project-scoped reusable criteria #218

Description

@alexeygrigorev

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:

  1. legacy, preserving the existing Homework table followed by the separate Projects table; and
  2. modules, 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

  • Add an explicit per-Cohort curriculum format with deterministic legacy migration/default behavior and intentional module opt-in.
  • Add cohort-owned ordered module/unit/learning-flow structure for module cohorts.
  • Render projects at their configured positions in the module learning flow; retain existing project destinations and lifecycle.
  • Replace cohort-wide criterion lookup with ordered project-to-criterion assignments that support both shared and distinct criteria.
  • Add narrow adapters for public views, Studio, exports/API, review submission, and scoring while retaining existing lifecycle models and algorithms.
  • Add expand/backfill/validate/contract migration and rollback controls, including production-like dry-run evidence.
  • Add serious Django, Playwright, accessibility, security, compatibility, and data-migration coverage for both formats and mixed-format operation.

Explicit non-goals

Dependencies and sequencing

Data/model ownership and invariants

Names below are logical contracts; implementation names may differ while preserving these invariants.

Cohort format and curriculum

  • curriculum_format is required on Cohort, never inherited implicitly from Course. Its supported values are exactly legacy and modules for this issue.
  • The migration and application default for every pre-existing Cohort is deterministically legacy. The migration takes an explicit reviewed allow-list/mapping for any selected existing Cohort converted to modules; heuristic inference from titles, slugs, existing assignments, or dates is forbidden.
  • Creating or duplicating a Cohort requires an explicit format value in Studio/admin API. A duplicated Cohort preserves the source format unless the operator explicitly chooses another supported value.
  • A legacy Cohort requires no Module, Unit, or ordered-flow rows and cannot publish module-flow data while its format remains legacy. Existing Homework, Project, criterion, submission, review, and score rows are not rewritten to simulate modules.
  • A Module belongs to exactly one module-format Cohort and has a stable slug plus an explicit deterministic position unique within that Cohort.
  • A Unit belongs to exactly one Module and has a stable slug plus an explicit deterministic position unique within that Module. The minimum Unit destination/content contract is resolved in the decision section before implementation.
  • Each Module has exactly one terminal Homework; that Homework belongs to the same Cohort and terminates at most one Module. Every published Homework in a module-format Cohort terminates exactly one Module, so no published Homework is hidden outside the flow. A module cannot be published/rendered as valid without its terminal Homework.
  • Module-format curriculum has one explicit, gap-tolerant but collision-free ordering contract that places every Module and Project exactly once. Modules render their Units in order and end with their Homework. Projects render at their stored positions in this same flow and never in a second Projects section.
  • A flow item cannot target both/neither a Module and Project, cannot target an object from another Cohort, and cannot duplicate a target already in the same flow. Moving/deleting an item is transactional and cannot leave a published invalid flow.
  • Legacy and module Cohorts can coexist under one Course and in one process/cache without one cohort's format, ordering, or objects leaking into another.

Project criteria

  • Criterion definitions do not belong to Cohort. An ordered project-criterion assignment is the source of truth for the criteria applicable to a Project.
  • A Project has zero or more criterion assignments with collision-free positions. The same criterion definition can be assigned to Project A and Project B in one Cohort; either project can also have criteria not assigned to the other.
  • Every review form, accepted review response, score calculation, results page, Studio editor, duplication path, and project-aware API/export obtains criteria from one shared criteria_for_project(project)-equivalent boundary. No consumer may fall back to “all criteria in the Cohort.”
  • CriteriaResponse and ProjectEvaluationScore retain their existing primary keys and criterion references. The adapter must not recalculate historical outcomes or change assignment/scoring/review algorithms.
  • A submitted criterion identifier is accepted only when it is assigned to the exact Project under review. Foreign, unassigned, stale, duplicate, or cross-Cohort criterion identifiers produce the documented validation response and no partial writes.
  • Shared-criterion editing/deletion must expose all affected Projects and preserve rows already referenced by responses/scores. The final immutability/copy-on-write rule is approved in the decision section; silent cascade deletion or silent rubric drift is forbidden.
  • Cohort duplication reproduces module/unit/flow structure and project-criterion assignments, preserves sharing within the duplicate graph, and creates no links back to source-Cohort curriculum definitions/history unless cross-Cohort reuse is explicitly approved. Learners and lifecycle history remain excluded as required by Add Course/Cohort lifecycle, public routes, and complete cohort duplication #53.

Migration and rollback constraints

  • Use an additive expand -> backfill -> validate -> contract sequence. The first application release must tolerate old rows while the backfill runs and must preserve the legacy read path until validation succeeds.
  • Backfill every existing Cohort to legacy except identifiers in the reviewed module allow-list. Re-running the backfill is idempotent and produces the same result.
  • For each existing Project, create ordered assignments to every criterion currently owned by that Project's Cohort, using the currently observable order (presently criterion primary-key order). Preserve criterion, response, score, project, review, and submission primary keys.
  • Preserve and report legacy criteria on Cohorts with no Projects; never delete or invent usage for them silently. The approved orphan policy is recorded before contract cleanup.
  • Preflight detects and reports: criteria/responses/scores referring across project/cohort boundaries, missing owners, duplicate/colliding positions, invalid module terminal-homework links, flow targets from another Cohort, and historical rows that cannot be represented. Contract migration is blocked until each discrepancy is corrected or explicitly quarantined with a stable synthetic identifier and audit record.
  • Forward/backward/fresh-install migration tests assert row counts, key sets, ownership, deterministic order, and checksums of unaffected legacy fields. Existing project totals/results are sampled before and after and must be identical.
  • Application rollback is non-destructive: additive tables/columns remain readable/ignored by the prior release, and legacy cohorts continue to render. Destructive schema reversal is allowed only before any module-format production data exists, or is refused with an actionable error; production rollback never drops module/criterion history.
  • The deployment runbook records dry-run counts, rejected/quarantined rows, duration, checksum comparison, rollout gate, rollback command/conditions, and redacted evidence. No production or registration data appears in logs/issues/screenshots.

Browser, URL, API, accessibility, and security behavior

  • Legacy Cohort pages preserve the existing release contract: Homework and Projects remain separate sections/tables with current labels, order, status/deadline/link behavior, URLs, metadata, cache headers, and JS-disabled behavior. Module UI/markup is absent. Newly added format/link rows do not change the visible page or source object values.
  • Module Cohort pages expose one deterministic learning flow. Modules show ordered Units and terminal Homework; Projects appear at configured positions in that flow with the same lifecycle status/deadline/action links as today; there is no separate Projects section.
  • Canonical Course/Cohort and workflow paths are those established by Add Course/Cohort lifecycle, public routes, and complete cohort duplication #53. Existing legacy/compatibility project and homework paths keep their established redirect-or-serve behavior, query handling, case/slash policy, canonical/noindex rules, and authorization. This issue adds no alternate URL tree.
  • Public API/Studio responses expose 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.
  • The legacy cohort-wide criteria export remains byte/schema compatible only where all Projects have the same ordered criteria. A module/non-uniform Cohort must never be silently represented as a union; the final project-aware endpoint or explicit compatibility error is approved below and documented for consumers.
  • Private submission/review/results pages remain no-store and 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.
  • Review routes bind Course/Cohort, Project, PeerReview, reviewer, submission, and every criterion to the same object graph. Altering a URL slug, review ID, form criterion ID, API object ID, or cached cohort context cannot cross a Cohort/Project boundary.
  • Both formats use semantic heading/list/table/disclosure markup appropriate to the presentation; all actions and module controls are keyboard operable with visible focus, names and state are exposed to assistive technology, status is not color-only, touch targets meet the adopted minimum, and desktop/mobile/200%-400% zoom do not hide curriculum or create two-dimensional page scrolling.

Acceptance criteria

Engineer-owned implementation evidence

  • E1 — Per-Cohort selection: Cohort persistence, Studio, admin API, create, edit, and duplicate operations require/return an explicit legacy|modules format; Course has no curriculum-format switch and changing one Cohort does not change any sibling.
  • E2 — Deterministic legacy backfill: Migration fixtures with no format become legacy on every run; only a reviewed explicit identifier mapping becomes modules; source Homework/Project/criterion/history values and PKs are unchanged and migration evidence records counts/checksums.
  • E3 — Mixed coexistence: One Course with a legacy Cohort and a module Cohort renders and serves APIs concurrently with isolated ordering, criteria, permissions, cache keys, and URLs.
  • E4 — Exact legacy behavior: Representative existing legacy fixtures produce the established Homework-then-Projects page contract, destinations, statuses, deadlines, compatibility responses, and no module markup before and after this feature.
  • E5 — Valid module ownership: Database/service validation enforces same-Cohort Module/Unit/Homework/Project flow ownership, stable unique ordering, exactly one terminal Homework per Module and one Module per published Homework, XOR flow target, exactly-once flow inclusion for every published Module/Project, and no published module flow on a legacy Cohort.
  • E6 — Module rendering/order: A fixture ordered Module A -> Project X -> Module B -> Project Y renders 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.
  • E7 — Shared and distinct criteria: For Project A=[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.
  • E8 — Criterion integrity: Forged/stale/duplicate/cross-Project criterion submissions are rejected atomically; shared edits/deletes follow the approved history rule; existing CriteriaResponse/ProjectEvaluationScore PKs and project totals are preserved.
  • E9 — Lifecycle preservation: In both formats, first submission, assignment of the configured number of non-self peer reviews, review submission, closure/scoring, and results complete through the existing state transitions/services with unchanged scores and side-effect timing.
  • E10 — Adapter boundary: Characterization tests prove peer-review selection, project state transitions, score/vote calculation, badges/leaderboards, and result logic are unchanged; code review shows curriculum/criteria integration is confined to surface adapters and shared criteria lookup rather than duplicate lifecycle implementations.
  • E11 — Duplication: Duplicating legacy and module Cohorts reproduces the selected format and complete definition graph/order, preserves shared-vs-distinct criteria inside the copy, permits independent edits, and copies no enrollment/submission/review/response/score/result history or source-Cohort links.
  • E12 — API/URL contract: Canonical, compatibility, reverse-resolution, slash/case/query, metadata, cache, and legacy API tests pass for both formats; non-uniform criteria are never flattened into a silent cohort-wide union.
  • E13 — Security/accessibility: Cross-Cohort/Project/review/criterion tampering yields safe denial and zero writes, private pages remain no-store/non-indexable, automated accessibility checks pass, and the manual checks below have evidence.
  • E14 — Migration/rollback: Forward, idempotent rerun, pre-contract rollback, fresh install, mismatch preflight, and “refuse destructive reverse after module data” cases pass; runbook and redacted production-like dry-run evidence satisfy the constraints above.
  • E15 — Regression gate: Focused Django suites, the full required Django verification plan, 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

  • T1: Tester independently reruns the focused Django matrix below and the full required verification plan, records exact commands/results, and confirms migration/source checksums and score outcomes without relying on engineer assertions.
  • T2: Tester runs Playwright at adopted desktop/mobile projects for legacy, module, mixed-Cohort, JS-disabled, lifecycle, URL, authorization, and accessibility scenarios; captures stable .tmp/screenshots/... evidence with no secrets or personal/production data.
  • T3 [HUMAN]: Tester completes keyboard-only operation, visible focus, named screen-reader announcement/state, contrast/status-without-color, touch target, text-spacing, and 200%/400% zoom/reflow checks for representative legacy and module pages and records browser/viewport/assistive technology.
  • T4: Tester exercises forged Course/Cohort/Project/review/criterion IDs and confirms safe responses, no existence disclosure beyond the existing contract, zero partial writes, and no cross-Cohort cache/render leakage.

Product-manager acceptance evidence

  • P1 [HUMAN]: PM approves the four product decisions below and confirms Decision: Confirm cohort-owned curriculum for the first consolidation release #14/spec 04 were amended before implementation begins.
  • P2 [HUMAN]: PM compares representative legacy before/after desktop/mobile evidence and accepts no visible/data/URL behavior drift.
  • P3 [HUMAN]: PM accepts module information architecture/order, project placement, terminal Homework behavior, and shared/distinct criterion editing language from the tester's evidence.
  • P4: PM verifies every acceptance checkbox has engineer plus independent-tester evidence where applicable, all P0/P1 regressions are resolved or explicitly waived, and rollout/rollback evidence is complete before marking accepted.

Required test matrix

Suggested test locations may change, but every row is required and must map to evidence.

Layer Concrete case Required assertions
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

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, and make test-playwright-core. The independent tester reruns rather than reuses engineer output.

Product decisions required before engineering

  1. Criterion reuse boundary [HUMAN]: approve same-Cohort sharing only for this release (recommended), or permit cross-Cohort reuse and define versioning/edit/history effects.
  2. Project placement [HUMAN]: approve Projects as ordered top-level entries between/after Modules (recommended), or allow a Project inside a Module and define whether it can precede the mandatory terminal Homework.
  3. Unit contract [HUMAN]: approve ordered title/slug/link metadata rendered in the Cohort flow with no new standalone authoring/page system (recommended), or specify owned body/video/routes.
  4. Compatibility export and history policy [HUMAN]: approve an explicit documented error plus project-aware export for non-uniform project criteria (recommended; never silent union), preserve unlinked legacy criteria for audit, and choose reject-in-place vs copy-on-write for editing a shared criterion already referenced by responses/scores.

Verification handoff

  • Engineer posts focused/full Django, migration, API/URL, and Playwright command output plus a criterion/lifecycle adapter diff summary. Engineer does not commit.
  • Independent tester posts rerun results, matrix mapping, and redacted .tmp/screenshots/... paths for desktop/mobile/manual checks.
  • PM records decision/spec updates, legacy parity review, module UX review, migration/rollback review, and final acceptance. Acceptance does not imply implementation exists until those roles complete the lifecycle.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Important follow-upaccessibilityArea: accessibilityadminArea: admincoursesArea: coursesdata-migrationArea: data-migrationdecisionOwner decision requiredenhancementNew feature or requestfrontendArea: frontendhumanCode complete; manual verification requiredsecurityArea: securitytestingArea: testing

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions