Skip to content

Discussion capability — sliced build/integration plan

Companion to ADR-0029 (Discussion is a native capability on the sync rail). Scope: build the native Discussion + the general Group primitive it consumes, the moderation/safety floor, and the thin CC landDiscussion hook that un-quarantines discussion-topic (ADR-0022 long tail).

Status: plan only (2026-06-17). Design is ratified (ADR-0029); nothing here is built. Branch per slice, rebased to land ([[feedback_rebase_never_merge]]). Each slice is independently shippable and validatable (bun run validate:full, bun test).

Group before Discussion (Discussion consumes it); offering-scope before group-scope (group-scope only changes the row filter + adds the role-aware asymmetry); safeguarding audit and moderation after the post model exists (they act on it); CC hook last (delegates to the finished capability — the ADR-0022 “land once the capability exists” pattern). A Slice 0 spike de-risks the raw pgxsinkit surface first, because the whole feature rides a combination that does not exist yet.

Slice 0 — pgxsinkit readwrite-membership spike (de-risk)

Section titled “Slice 0 — pgxsinkit readwrite-membership spike (de-risk)”

Prove, on a throwaway or the real post table, the surface ADR-0029 depends on: a readwrite sync table with a customWhere offering-membership filter (cross-person fan-out on the write path), delete propagation, LWW convergence, a trigger writing to a non-synced table, and subqueries in an RLS insert policy. Anything that bends is fixed upstream in pgxsinkit ([[project_pgxsinkit_full_commitment]]), not worked around. Output: a go/fix-list for Slices 2–5.

Slice 1 — Group domain (general primitive)

Section titled “Slice 1 — Group domain (general primitive)”

group_set + group + group_membership (offering-scoped); a teacher group-formation surface in teacher-portal (manual assignment only). Erasure entries (group_membership; sever/cascade per the registry pattern). RLS + sync registration as needed. Standalone value: Groups are usable by future consumers (group tasks) independent of Discussion. Validate: schema-smoke + erasure-registry tests green; a teacher can form/edit Groups.

Slice 2 — Discussion + Post core (offering-scoped)

Section titled “Slice 2 — Discussion + Post core (offering-scoped)”

discussion (on an activity Unit, scope discriminator defaulting to offering, sanitized HTML prompt) + post (parent_post_id, author_person_id, body, status, edited_at, auditAndLwwColumnsUs). First readwrite + offering-membership sync table. RLS: read = member, insert = self + member, update/soft-delete = author. Author edit, hard-delete leaf / tombstone-with-replies. Erasure: Post = scrub body + sever author. Shared role-agnostic thread renderer in packages/ui; learner-web participate layout; teacher-portal read view. Validate: PGlite real-execution tests for RLS + erasure tombstone; offline compose → sync round-trip.

Add scope = group (targets a Group Set) + the role-aware row filter: a learner syncs only their own Group’s posts; a teacher/admin syncs across all Groups (“in a Group I’m in OR an Offering I teach/admin”). Teacher cross-group view. Validate: a learner cannot see another Group’s posts; a teacher sees all.

Slice 4 — Safeguarding edit/delete audit

Section titled “Slice 4 — Safeguarding edit/delete audit”

A Postgres trigger on post writes change_audit_entry on insert/update/delete (server-side, not synced). Moderation-read scope = teacher + org-admin (single centralized predicate). Erasure scrubs the audit for the erased person. Validate: an edit/delete is captured with before/after; the trail is invisible to learners and to non-scoped roles; erasure scrubs it.

Report (offline-capable, owner-scoped sync; reporter-anonymous; sever+scrub erasure) + learner report UI. Moderator hide + resolve + thread-lock + offering-scoped ban/mute (posting_restriction, owner-scoped synced to the restricted person for graceful offline compose disabling; enforced via RLS insert subquery). Teacher moderation UI. Graceful offline write-rejection surfacing (locked/banned). Validate: a banned learner’s compose is disabled offline; a locked thread rejects replies with a surfaced bounce; a report reaches the moderator anonymously.

Per-(person, Discussion) last-seen marker (owner-scoped sync), driving a “new posts” badge. Validate: badge clears on open, reappears on new synced posts.

Slice 7 — CC landDiscussion + conformance

Section titled “Slice 7 — CC landDiscussion + conformance”

Thin landDiscussion in cartridge-import: parse imsdt <topic> → one offering-scoped Discussion on the resource’s activity Unit; report imported.discussions[]; cartridge_import gains a discussion count. Move discussion-topic from the quarantine column to imported in the CC conformance lane (expectNothingSilentlyDropped still holds). Validate: the CC conformance + .local corpus import discussions natively with nothing dropped.

  • Neutral vocabulary (CONTEXT.md): Discussion / Post / Report / Posting Restriction / Group / Group Set / Group Membership. Nothing role-specific in shared components.
  • Erasure: every new person-linked table declares a class (CI smoke enforces it).
  • No research Consent gate — participation/moderation is operational under the org’s delegated authority (ADR-0008/0027); a research_excluded flag rides the synced rows as elsewhere.
  • DB migrations are Anton’s domain ([[feedback_db_migrations_anton_domain]]); generate per the runbook, never hand-edit.

Out of scope (→ ADR-0029 Deferred / ADR-0030)

Section titled “Out of scope (→ ADR-0029 Deferred / ADR-0030)”

MOOC online forum, real-time Chat, “grade this discussion”, self-select/random groups, cross-device drafts, present-back, automated abuse flagging, restriction appeals, and the whole notifications/subscription block (ADR-0030).