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).
Sequencing rationale
Section titled “Sequencing rationale”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.
Slice 3 — Group-scoped discussion
Section titled “Slice 3 — Group-scoped discussion”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.
Slice 5 — Moderation & safety floor
Section titled “Slice 5 — Moderation & safety floor”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.
Slice 6 — Unread badge
Section titled “Slice 6 — Unread badge”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.
Cross-cutting (every slice)
Section titled “Cross-cutting (every slice)”- 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_excludedflag 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).