Clean successor to V3 (forge: lilith/atlilith). Seeded from local Mac working tree at ~/Code/@projects/@cocottetech/. node_modules and build artifacts excluded via .gitignore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 KiB
Peer block + report flow
End-to-end sequence for blocking a peer and (optionally) reporting them for review. Anchors brief AE §AE10 — the sanction surfaces ladder (block → mute → coop-mod report → platform-admin report → directory shadowban → coop removal → platform-wide suspension). Pairs with the AE2 connection state machine; siblings are erasure-cooling-off.flow.md (terminal staged action) and vacation-mode.flow.md (declarative mass-pause).
Voice register throughout: plain (voice §V2c). Per AE10 explicit: no metaphor, no hearth-softening, no editorial flourish. Moderation copy reads like a safety system, not a hearth. The brand swaps registers here on purpose — being misread would be costly.
Goal
Give the reporter a low-friction path to (a) hard-cut visibility with another peer (block) and/or (b) escalate that peer's conduct to a review body (coop moderators or platform-admin), with append-only audit on every transition. Block is unilateral and instant; report is adjudicated by humans on the other side. The two are independently invokable: a reporter can block without reporting, report without blocking, or do both in one sweep.
Actors
- Reporter — the provider invoking the action (in plain register, addressed as "you").
- Target peer — the peer being blocked / reported. AE2 connection state with the reporter is any of
none,requested,connected,muted. - Coop moderators — quorum of active mods in a shared coop (per AE4 / brief N). Lean BLR-Q1: 3 of 5 active mods.
- Platform-admin — out-of-scope brief; signposted recipient for cross-coop or platform-policy violations.
Preconditions
- Reporter is signed in, posture is
discoverableoropen(incognito reporters can still block; reporting from incognito is allowed but the reporter's identity surfaces to mods, not to the target — see BLR-Q2). - Reporter has some connection-state with target (
requested,connected,muted, or merelynonewith shared-surface contact: salon co-membership, coop co-membership, feed appearance, or a directory profile view within the last 30 days). - For coop-mod reports: reporter and target share at least one coop.
- For platform-admin reports: no shared-coop requirement.
Flow
- Trigger. Long-press on (a) a peer row in coop-drawer / peer-roster, (b) a peer-DM message bubble, (c) a salon message bubble, or (d) a directory profile result row. Long-press summons the peer action sheet.
- Action sheet. Plain-register sheet with three rows:
- Mute (per-peer or per-salon — scope picker on tap)
- Block (per-peer)
- Report (per-peer) Mute and block are instant; report opens a sub-flow. Cancel dismisses.
- Block confirmation modal. If reporter taps Block:
Single tap commits. No typed-confirm — block is reversible, unlike erasure.Block {Target}. {Target} will not see your profile, feed, salons, or DMs. You will not see theirs. Existing peer-DM thread closes. [ Cancel ] [ Block ] - Block applied — fan-out. On confirm, ai-copilot writes
peer_connectionsrow state →blocked(per AE2 state machine). Side-effects, all idempotent:- Existing peer-DM thread → closed (rows preserved, surface hidden on both ends).
- Salon membership co-presence → reporter's view of target's messages is suppressed on read; target's view of reporter likewise (symmetric occlusion regardless of who blocked whom).
- Feed aggregation (AE9) → target's contributions excluded from reporter's rollups.
- Endorsements (AE7) between the pair → frozen, not deleted; not re-displayable while blocked.
- Directory (AE5) → target's profile no longer surfaces to reporter; reporter's profile no longer surfaces to target.
- Audit row — block.
agent_actionsinsert:action_type='peer_blocked',target_kind='peer',target_id={target_user_id},stakes='high',confidence=null(human action),outcome_json={ scope: 'all_AE', reversible: true }. Append-only. - Target notification — none on block. Per AE10 explicit: target is not notified of a block. The occlusion is silent on the target's side; their view of the reporter simply ceases without an event marker. This is intentional — notifying creates retaliation risk.
- Report sub-flow — recipient picker. If reporter taps Report (instead of or in addition to Block):
If reporter and target share zero coops, the coop row is disabled with the inline note "No shared coop. Route to platform admin." Cross-coop violations route platform-admin by default.Report {Target}. Who reviews this? ( ) Coop moderators — {coop_name} ( ) Platform admin — cross-coop or policy [ Cancel ] [ Continue ] - Reason picker. Five categories, single-select (per AE10 sanction-ladder mapping):
- Harassment
- Spam
- Impersonation
- Boundary violation
- Other "Other" requires the optional-notes field (Step 9) be non-empty.
- Optional notes — PII-gated. Free-text ≤ 500 chars. As the reporter types, ai-copilot runs the K3 PII scan client-side (govt names, precise location, contact bytes). On a K3 hit, the submit button disables and a plain-register inline note appears:
One of those words won't pass. Rewrite without it. No surfacing of which word — per brief AD §AD7-style fallback, the system never names the language of the trigger. Reporter re-drafts; gate re-runs on each keystroke debounce.
- Submit. On submit, ai-copilot writes
peer_reportsrow:reporter_id,target_id,recipient_kind(coop_mods|platform_admin),recipient_id(coop_id or null),category,notes,state='pending',submitted_at. Dispatch eventpeer.report.submittedto Redis pub/sub; coop-mod queue subscribes forcoop_modsrecipient, platform-admin queue for the other. - Audit row — report.
agent_actionsinsert:action_type='peer_reported',target_id={target},stakes='high',confidence=null,outcome_json={ category, recipient_kind, recipient_id, report_id }. Per AE10 voice rule, the chat-home receipt is plain:Reported {Target} to {coop_mods | platform admin}. Category: {category}. Review opens within 24 hours.
- Target notification — yes on report. Per AE10 explicit (contrast with block): target receives a plain-register notification on report submission:
Someone reported you to {coop_mods | platform admin} for {category}. Review pending. You will hear from {recipient} when a decision lands. Reporter identity is not disclosed to target (BLR-Q2 lean: anonymous to target, identified to mods). Target's peer-DM dispatch to the reporter is paused for the duration of review.
- Quorum review (coop-mods recipient). Cooperative mod state machine:
pending → quorum_met → decision. Mods receive the report in their coop-mod queue. Quorum threshold (BLR-Q1 lean: 3 of 5 active mods) must align on a decision. If quorum is met within 14 days, state moves todecisionwith one of:dismissed,directory_shadowban,coop_removal. Higher sanctions (platform-wide suspension) require platform-admin escalation regardless of mod recommendation. - Platform-admin review (platform-admin recipient). Out-of-scope brief; signpost only. Platform-admin can apply any sanction ladder rung up to platform-wide suspension. Same state machine, different actor.
- Sanction surface — applied. When a decision lands, ai-copilot writes the sanction to the target's user row +
peer_sanctionstable and emits a plain-register chat-home notification to the target (not the reporter — reporter sees the outcome receipt in Step 17):dismissed— "Report against you was dismissed. No action."directory_shadowban— "Your directory profile is hidden platform-wide. Other peers cannot find you through search or feed. Existing connections persist. Appeal within 14 days."coop_removal— "Removed from {coop_name} by mod quorum for {category}. Other coops unaffected. Appeal within 14 days."platform_suspension— "Account suspended. Peer-facing surfaces and ai-copilot dispatch are off. Appeal within 14 days. Erasure (brief V) remains available."
- Appeals window — 14 days. Per BLR-Q3 lean. Target can submit an appeal via Settings → S5 → "Active sanctions" row. Appeal goes to platform-admin regardless of original recipient (coop mods cannot adjudicate appeals to their own decisions). State moves to
appeal_pending. - Outcome receipt — reporter. Reporter sees a plain-register chat-home line on decision:
Coop mods reviewed your report on {Target}. Decision: {decision}. Your block stays in place regardless.
- Appeal decided. Platform-admin returns a verdict:
appeal_upheld(sanction reversed, target restored) orappeal_denied(sanction stands). Either way: append-only — the original sanction audit row is not deleted; a supplementaryappeal_decidedrow joins it. State moves toappeal_decided. Target sees a plain-register notification; reporter sees one as well.
State transitions
| Step | Entity | State column |
|---|---|---|
| 3 → 4 | peer_connections.state |
* → blocked (block-pending → block-applied within same dispatch) |
| 5 | agent_actions |
row written: peer_blocked |
| 10 | peer_reports.state |
null → report-submitted |
| 13 | peer_reports.state |
report-submitted → under-mod-review → quorum-met |
| 13 | peer_reports.state |
quorum-met → sanctioned OR quorum-met → dismissed |
| 15 | peer_sanctions.state |
null → applied |
| 16 | peer_sanctions.state |
applied → appeal-pending |
| 18 | peer_sanctions.state |
appeal-pending → appeal-decided (with appeal_outcome ∈ upheld/denied) |
| 18 | peer_connections.state |
unchanged by appeal — block is reporter's unilateral action, separate from sanction lifecycle |
Block and report run on independent state machines. A reporter can unblock at any time without affecting an in-flight report; a sanction can be reversed on appeal without auto-unblocking the reporter (the reporter chose the block; only the reporter ends it).
Voice notes
Every Cocotte-side line is plain register. No "drawer," no "simmering," no "stockpot," no "kept warm." No softening adverbs ("just," "simply," "a little"). No corporate-apology shapes ("we regret," "unfortunately"). Short sentences. Exact nouns. Exact counts. The brand reads like a safety system here because moderation is a safety system.
Sample lines, verbatim, for the surface to use:
- "{Target} will not see your profile, feed, salons, or DMs."
- "Reported {Target} to coop mods. Category: harassment. Review opens within 24 hours."
- "Someone reported you to coop mods for boundary violation. Review pending."
- "One of those words won't pass. Rewrite without it."
- "Coop mods reviewed your report on {Target}. Decision: directory shadowban. Your block stays in place."
- "Removed from {coop_name} by mod quorum for spam. Other coops unaffected. Appeal within 14 days."
- "Your directory profile is hidden platform-wide. Existing connections persist."
- "Appeal denied. Sanction stands. Erasure (Settings → Privacy & data) remains available."
- "Block lifted. {Target} can see your profile again. Salon and DM access does not auto-restore — re-connect if you want it."
- "Report withdrawn. Coop mods will see the withdrawal in their queue. The audit row stays."
Edge cases
- Target already blocked — re-invoking Block is idempotent. The action sheet's Block row is replaced with "Unblock" when target is already in
blockedstate; tapping it opens a parallel one-tap modal. No duplicatepeer_blockedaudit row; tapping Block when already blocked is a no-op (silently). - Cross-coop report routing — if reporter and target share zero coops, the recipient picker forces platform-admin. If they share multiple coops, the picker enumerates each coop as a distinct row; reporter picks one. Cross-coop violations (target's conduct affects coops the reporter isn't in) always route platform-admin.
- Both peers in shared coop block each other — symmetric occlusion. Neither sees the other anywhere on AE surfaces — coop peer-roster, salons (both still members; messages mutually suppressed on read), feed, directory, DM. Coop mods still see both peers normally.
- Reporter withdraws within 24h grace — Settings → S5 → "Reports filed" → tap report row → "Withdraw" button (visible only while
state ∈ {report-submitted, under-mod-review}and within 24h of submit). On withdraw:peer_reports.state → withdrawn; coop-mod queue receives the withdrawal event; audit rowpeer_report_withdrawnjoins the report row (original is not deleted). Reporter's block, if separately applied in Step 3, remains. - Target appeals successfully — sanction reverses on user row +
peer_sanctions. Audit not deleted: originalpeer_sanctionedrow stays, supplemented byappeal_decidedrow withappeal_outcome='upheld'. Per brief I append-only — supplements never deletes. The that-it-happened shape survives even when the consequence reverses. - Mod quorum incomplete after 14d — auto-escalates to platform-admin.
peer_reports.state → escalated_to_platform_admin, dispatched to the platform-admin queue. Audit rowpeer_report_escalatedwritten. Reporter and target both see plain-register notifications: "Coop mods did not reach quorum in 14 days. Platform admin is now reviewing." - K3 leak in report notes — per Step 9, the gate re-draft loop fires on each keystroke debounce. Per brief AD §AD7, the system never names which word triggered the gate, never names the language, never offers a thesaurus hint. The reporter re-drafts blind; the gate either passes or it doesn't. This is intentional — naming the trigger leaks the K3 ruleset and trains evasion.
- Reporter erases account during open report — per brief V §V2f coop carve-out: filed reports stay published. The report does not delete on erasure; it persists in the coop-mod queue. The reporter's
peer_reports.reporter_idis preserved in audit (redacted shape, see brief I §I append-only), but display-name on the mod queue resolves to "(erased reporter)" from the redaction state. - Target erases account during open report — sanction lifecycle short-circuits to
target_erased. Mod queue closes the report. Audit row preserves the report's that-it-happened shape per V2e redaction. - Salon-message report vs peer-DM report — same flow; the report row carries an additional
context_kind ∈ {salon_message, peer_dm, profile, feed, directory}andcontext_idfield so mods see the bytes-in-context they need to review.
Audit
Every transition in this flow emits exactly one agent_actions row. Action types in order of possible firing:
peer_blocked— stakes high, confidence nullpeer_unblocked— stakes high, confidence nullpeer_reported— stakes high, confidence nullpeer_report_withdrawn— stakes medium, confidence nullpeer_report_escalated— stakes high, confidence null (system-initiated on 14d quorum miss)peer_mod_decision— stakes high, confidence nullpeer_sanctioned— stakes high, confidence nullpeer_sanction_appealed— stakes high, confidence nullappeal_decided— stakes high, confidence null
confidence is null on every row in this flow: per the project's stance, confidence is a model-output column, not a human-decision column. Moderation actions are intentional human decisions or rule-driven system decisions; the model does not score them.
Append-only per brief I §I. Appeals supplement, never delete. A sanction reversed on appeal leaves both the original peer_sanctioned row and the appeal_decided row in audit, joined by appeal_id. Replay shows the full arc — what happened, what was contested, what changed.
Related
- brief AE §AE10 — sanction-surfaces ladder + plain-register stance. §AE2 — connection state machine that block plugs into. §AE11 — opt-in postures and how block + incognito interact.
- brief K — K3 PII gate on the optional-notes field (Step 9).
- brief I — append-only audit; this flow's Audit section implements against §I.
- brief V §V2f — coop carve-out: filed reports survive reporter's erasure. Sibling flow
erasure-cooling-off.flow.mdshares the audit-redaction-not-deletion pattern. - brief M — degraded modes when the coop-mod queue is backed up or the platform-admin queue is offline. Block remains available locally regardless; report dispatch retries with backoff and surfaces a plain-register "Report queued — dispatch delayed" line if the queue is unreachable for >5min.
- brief AD §AD7 — never-names-the-language fallback shape adopted for K3 leaks in report notes.
- brief N — coop structure and mod-quorum mechanics.
coop-drawer.screen.md— entry point for peer-row long-press.audit-row-detail.screen.md— sanction + audit surface that renders the rows this flow emits.vacation-mode.flow.md,erasure-cooling-off.flow.md— sibling flow patterns this doc inherits structure from.- voice §V2c — plain register throughout.
Out of scope
- Criminal-law reporting — not the platform-admin's purview. If a report category implies criminal conduct (threats of violence, CSAM, trafficking, etc.), platform-admin's response surface includes signposting to external resources, but actual law-enforcement liaison is out of scope at P0.
- Real-world identity verification of reports — mods and platform-admin review the bytes-in-context (salon message, peer-DM, profile) plus the reporter's notes. No KYC of the reporter to "qualify" the report, no in-person verification of claimed incidents. The platform adjudicates platform conduct only.
- Mod-recruitment / mod-rotation mechanics — handled in brief N (coop). This flow assumes mods exist and quorum is computable.
- Mute mechanics in detail — Mute is mentioned in the action sheet (Step 2) but has its own lighter flow (no audit row for per-peer mute; per-salon mute writes a salon-membership-modification row). Out of scope here; covered in
peer-mute.flow.md(TBD).
Open questions
- BLR-Q1 — mod-quorum threshold. Lean: 3 of 5 active mods, or unanimous if a coop has fewer than 5 active mods. Active = posted or moderated within last 30d. [blocking] — required for AE4/AE10 to ship.
- BLR-Q2 — anonymous reports allowed? Lean: no — reporter identity surfaces to mods (so mods can detect coordinated/retaliatory reports), but stays anonymous to target. Reporter sees the lean clearly at report time so they don't expect anonymity-from-mods. [blocking] — affects schema + mod queue UI.
- BLR-Q3 — appeal window length. Lean: 14 days, matching erasure cooling-off cadence and
coop_removalprecedent. [nice-to-have] — defensible at 7d or 30d; 14d is the brand-coherent middle. - BLR-Q4 — should the action sheet (Step 2) include a "Combined block + report" single-tap row to reduce friction in the common case? Lean: yes for P1, not P0; keep the flows independently auditable at P0 and bundle later once the per-flow telemetry is in. [exploratory]
- BLR-Q5 — should target be told category of report (Step 12) or only that a report exists? Lean: tell category — vagueness ("someone reported you") reads more menacing than the specific charge, and the brand's stance is plain register over softened. [nice-to-have]
- BLR-Q6 — withdrawal grace period: 24h (current lean) vs 7d? Lean: 24h, matching the "reflexive misclick" window without enabling reporter-side weaponization (file → wait → withdraw → re-file as harassment vector). [nice-to-have]