cocottetech/@platform/codebase/@features/ai-copilot/docs/peer-profile.screen.md
natalie 1b719e1fd7 chore(bootstrap): initial V4 commit
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>
2026-05-18 08:11:41 -07:00

169 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# peer-profile.screen
Implementation breakdown of [Brief AE §AE5](./AE-provider-social-network.brief.md) profile rendering + [Brief AE §AE2](./AE-provider-social-network.brief.md) connection-state controls — what a provider sees when viewing **another** peer. Reached from [`peer-directory.screen.md`](./peer-directory.screen.md), from coop-drawer peer-roster rows, from a referral-introduction sheet (AE1b), from peer-DM thread headers, and from endorsement / mentorship notification rows in chat-home.
Voice register: **working** for the bio, surfaces, endorsements, mutual-coop chips, and connection controls (per [`00-system-voice.md`](./00-system-voice.md) §V2b — peer relationships are deliberate). **Plain** for moderation actions, blocked / 404 states, and posture-derived empty views (per §V2c — block + moderation cannot be softened).
## Layout (iPhone 17 logical 393×852)
```
┌─────────────────────────────────────────────────┐
│ ◄ status bar (system) │ 47pt
├─────────────────────────────────────────────────┤
│ [◄] Profile [⋯] │ 56pt — top bar
│ │ ⋯ = moderation menu (long-press anywhere)
├─────────────────────────────────────────────────┤
│ │
│ @sarah-k ★ 12 │ header band
│ Berlin · DE / EN · she/her │ region · langs · pronouns
│ posture: open · last active 2h ago │ posture badge + freshness
│ │
│ ──── Bio ──────────────────────────────── │
│ "Two years on Tryst, three on OF. Pricing- │ bio body — passes K3 + V6 gate
│ study group regular. Berlin-area tours │
│ monthly. Mandarin OK, German native." │
│ │
│ ──── Surfaces operated ────────────────── │
│ [tryst] [of] [threads] │ surface chips per O
│ │
│ ──── Endorsements · 12 ────────────────── │
│ "bookings-tryst listing optimization" · 5 │ scope-claim · endorser count
│ endorsed by @q-berlin · @mira-pdx · +3 │ small-text endorser sample
│ "Mandarin-speaking subject handling" · 4 │
│ endorsed by @q-berlin · @lin-sf · +2 │
│ "Tour pricing for Berlin" · 3 │
│ endorsed by @mira-pdx · @rosa-de · +1 │
│ [ See all 12 → ] │
│ │
│ ──── Mutual coops · 2 ─────────────────── │
│ [Berlin escort coop] [Pricing study] │ chip row; tap → coop drawer
│ │
│ │ (scroll padding)
│ │
├─────────────────────────────────────────────────┤
│ [ Follow ▾ ] [ Request colleague ] │ 64pt — footer control bar
└─────────────────────────────────────────────────┘
│ 34pt — home indicator
```
## Components
| Component | Brief ref | Notes |
|---|---|---|
| Top bar | AE §AE5 | Back to source surface. `[⋯]` opens the moderation + utility menu: "Mute", "Block", "Report to coop moderators", "Report to platform-admin", "Share profile via referral" (AE1b), "Copy handle" (handle only, never any other field). |
| Header band | AE §AE5 + AE7 | Handle · endorsement-count badge (★N). Second line: region (coarse) · languages · pronouns. Third line: posture badge + freshness (`last active ~Nh ago`, rounded to vigil; never precise). |
| Posture badge | AE §AE11 | One of `open` / `discoverable` / `incognito`. Color-neutral; this is information, not status. Only rendered if the viewer has visibility into the posture (i.e. the peer is in `discoverable` for a shared-coop view, or `open` for a directory view). |
| Bio section | AE §AE5 | Free-text, passed through K3 + voice §V6 gate at publish-time. Renders as quoted text in working register. Long bios expand with "See more"; never auto-truncate mid-sentence. |
| Surfaces operated chips | AE §AE5 + O | One chip per `peer_profiles.surfaces_operated` entry. Claim-only at P0 (no verification — per AE5). Tappable: opens a sheet describing the surface kind, NOT a deep-link into the peer's content (cross-surface content links across providers are out-of-scope at P0). |
| Endorsements list | AE §AE7 | Grouped by `scope_claim`; per group: claim text + endorser count + first 23 endorser handles + overflow. "See all" routes to a full endorsements sheet. Renders only `public=true AND withdrawn_at IS NULL`. |
| Mutual coops chip row | AE §AE5 + N | Coops the viewer and the peer both belong to (intersection per N membership). Tap → coop drawer for that coop. If 0: section hidden entirely (don't render an empty mutual-coops zone — signals nothing useful). |
| Connection control footer | AE §AE2 | Polymorphic per `peer_connections.(kind, state)` for the viewer↔peer pair. Two slots: a `follow` control (lightweight) and a `colleague` control (gated). Labels per State table below. |
| Moderation menu | AE §AE10 | Long-press anywhere on the profile body (or `[⋯]`) opens it. Items in plain register: "Mute (per AE2)", "Block (per AE2)", "Report to coop moderators", "Report to platform-admin", separated visually from utility items. |
## States
1. **Viewing a directory-discoverable peer, `none` connection (default)** — full layout as drawn. Footer: `[ Follow ▾ ]` + `[ Request colleague ]`. Both actions live.
2. **Viewing a follow-only peer (you follow them, they don't follow back)** — footer: `[ Following ▾ ]` (tap = unfollow / request colleague) + `[ Request colleague ]`. Endorsements + bio fully rendered.
3. **Viewing a connected colleague** — footer: `[ Following ▾ ]` + `[ Connected ▾ ]` (drop: disconnect / mute / start peer-DM). New section appears above the footer: "Threads" — 13 recent peer-DM thread snippets (rendered per chat-home thread row pattern). Endorsement-offer affordance: `[ Endorse @sarah-k ]` inline at the bottom of the endorsements section.
4. **Connection requested by viewer** — footer: `[ Follow ▾ ]` + `[ Requested ]` (disabled). Header has small plain-register stamp: "Requested 2h ago. They'll see this when they next open Peers."
5. **Connection requested by peer (incoming)** — footer: `[ Follow ▾ ]` + `[ Accept ▾ ]` (drop: accept / decline / decline-with-note). Header has working-register banner: "Sarah-K asked to be colleagues. They endorse `bookings-tryst` listing optimization." (Sample-endorsement line is generated from peer's accepted endorsements, not from the request itself.)
6. **Muted by viewer** — footer: `[ Following ▾ ]` + `[ Muted ▾ ]` (drop: unmute / block / disconnect). Endorsements + bio render normally; this is muting, not blocking. Header has working-register chip: "You muted Sarah-K. They don't know."
7. **Blocked by viewer** — full-screen plain-register takeover: "You blocked Sarah-K. They won't see your profile, feed, or any AE surface." Single CTA `[ Unblock ]`. No bio / endorsements / mutual-coops visible — blocker has chosen to not see, honor that.
8. **Viewed-by-blocked-peer (404-style)** — viewer was blocked BY this peer. Plain register full-screen takeover: "This profile isn't available." No bio, no endorsements, no posture, no last-active, no mutual coops, no footer controls — only `[ ← Back ]`. The copy is intentionally ambiguous: does not confirm the peer exists, does not confirm a block. Mirrors HTTP 404 semantics so blocking is non-distinguishable from "deleted profile" or "never existed". Per AE §AE10: a block must not leak its existence to the blocked peer.
9. **Viewed-by-non-discoverable-peer (mutual-coop-only view)** — the peer's posture is `incognito` or `discoverable` and the viewer has no platform-wide visibility, but ≥1 shared coop exists. Renders a stripped layout: handle + posture badge (`incognito` rendered as "—" / "private") + mutual coops chip row + footer with `[ Request colleague ]` *only if* the peer is `discoverable` AND a shared-coop opt-in exists (AE1a); otherwise footer is suppressed. Bio, endorsements, surfaces all hidden. Plain register helper line: "This peer keeps their profile to coops you share."
10. **Viewing your own profile** — redirects to the self-edit profile screen (`peer-profile-edit.screen.md`, out-of-scope here). If somehow rendered: read-only with "This is you — edit in Settings".
11. **Peer is under moderation review** (AE §AE10) — header has plain-register banner: "Profile under review. Endorsements paused." Endorsement section renders with a "paused" overlay; bio + surfaces render normally. Connection actions still live — moderation is on their profile claims, not on contactability.
12. **Peer just dissolved their account / erased (Brief V)** — full-screen plain takeover: "This profile has been erased." Identical visual to state 8 (no leakage about whether it's erasure vs block). Past peer-DM threads still visible in viewer's chat-home audit; this screen only governs profile rendering.
13. **Degraded (M §M2b)** — backend unreachable. Top banner (yellow, plain): "Peer profile can't refresh. Showing cached data from {time}." Connection actions disabled with hint "back online to act."
14. **Offline (M §M2c)** — no cached data: plain register full-screen "Offline. This profile needs a connection." Cached: behave as state 13.
## Interactions / gestures
- **Tap surface chip** → surface-kind info sheet (not a deep link; AE doesn't bridge content across providers at P0).
- **Tap endorsement claim row** → full endorsement detail sheet (claim text, evidence if any, endorser handles, accepted-at timestamps).
- **Tap mutual-coop chip** → opens `coop-drawer.screen.md` for that coop.
- **Long-press anywhere on profile body** → moderation menu (same as `[⋯]`).
- **Tap footer `Follow` / `Request colleague`** → confirmation sheet ("one-line context" for request; follow is one-tap with subtle haptic).
- **Tap `Connected ▾` drop** → context menu: "Start peer-DM", "Endorse @handle", "Disconnect", "Mute", "Block".
- **Swipe-down** → dismiss to source surface.
- **VoiceOver order** — handle → endorsement-count → posture → freshness → bio → surfaces → endorsements (claim-by-claim) → mutual coops → footer controls. Identity-and-posture-first, action-last.
- **Pull-to-refresh** → re-pulls profile; surfaces any state changes (new endorsements, posture changes, connection state advances) with a subtle "updated" pip.
- **Tap header endorsement-count badge ★N** → scrolls to endorsements section + expands all groups.
## In-the-wild copy
- (working, default empty bio) "No bio yet." — never invent one.
- (working, connection request received header banner) "Sarah-K asked to be colleagues. They endorse `bookings-tryst` listing optimization."
- (plain, blocked-by-viewer) "You blocked Sarah-K. They won't see your profile, feed, or any AE surface."
- (plain, 404 / blocked-by-peer / erased) "This profile isn't available."
- (plain, non-discoverable mutual-coop-only) "This peer keeps their profile to coops you share."
- (plain, under-review banner) "Profile under review. Endorsements paused."
- (working, endorse affordance) "Endorse @sarah-k for…" — sheet opens with scope-claim entry.
- (plain, degraded) "Peer profile can't refresh. Showing cached data from 14:02."
- (working, requested-stamp) "Requested 2h ago. They'll see this when they next open Peers."
- (plain, mute-receipt) "You muted Sarah-K. They don't know."
- (plain, moderation menu — block confirmation) "Block Sarah-K? They won't see your profile, your feed, or that you exist on this platform. You can unblock later."
## Privacy invariants
- **Blocked peers see state-8 takeover — never anything else.** No leakage of bio, endorsements, posture, or mutual coops. The 404 copy is identical to erased-profile to remove the block signal.
- **`last_active_at` is rounded to vigil (~6h buckets) — never precise.** Per AE11 + I append-only audit, precise activity is internal-only.
- **No screenshot affordance + iOS screen-capture detection** raises a plain banner (per `coop-drawer.screen.md` privacy pattern): "Screenshots of peer profiles are discouraged. This is a private directory."
- **Handle is the only copyable identifier.** Bio, endorsement text, region, langs — none are tap-to-copy. Long-press shows a privacy reminder, not a copy menu.
- **Endorser handles in the endorsement list respect the endorser's posture.** If `@q-berlin` is `incognito`, their endorsement still counts toward the public count but their handle renders as `(private peer)` — the endorsement is real, the endorser is hidden.
- **Mutual-coop chips never reveal coops the viewer is not in.** The intersection is computed server-side; rows that fail RLS never reach the client.
- **No "viewed your profile" telemetry to the peer.** Profile view is audit-only (I); never push-notified to the peer. Only connection requests, endorsement offers, and DM sends generate peer-visible events.
- **PII filter applies at render time too**, not only at publish. If K3 list updates post-publish, the bio renders as "Bio under review" stub (per directory screen state 7 pattern).
## Edge cases
- **Peer changes posture from `open``incognito` while viewer is on the profile** → screen live-transitions to state 9 (or state 8 if no shared coop) with a subtle fade; no error. Connection state (if any) persists per AE11 — past data doesn't disappear, surfacing stops.
- **Peer-DM thread exists but peer just blocked viewer** → profile renders as state 8 (404), BUT the past thread in chat-home remains visible to viewer (it's audit-preserved). Viewer cannot send new messages; composer disabled in that thread with plain copy "Thread paused."
- **Endorsement count includes an endorsement from a peer who has since been suspended (AE10)** → count holds (it was valid when made); the suspended endorser's handle renders as `(peer suspended)` in the endorser sample.
- **Mutual-coop chip for a coop the viewer just left mid-session** → chip removed on next refresh; if tapped mid-stale, opens the coop drawer in "left" state (per `coop-drawer.screen.md` state 6).
- **Peer's handle changed since the viewer last loaded** → new handle renders; small footer hint on first load post-change: "@old-handle is now @new-handle." (One-time, not persistent.) Handle history is audit-only.
- **Request-colleague with a peer who has rate-limit-capped incoming requests** (AE10 anti-spam) → request still submits; peer's queue holds it; viewer sees `Requested` state. No leakage that the peer is at cap.
- **Long-press moderation menu opened on viewer's own self-profile** → menu hides Mute / Block / Report items (those are not self-actionable); shows "Edit profile" + "Posture settings".
- **VoiceOver + endorser handle that includes punctuation** → handles render character-by-character on first encounter, then by token; matches handle-screen convention.
- **Reduced motion** — replace section-expand animations on "See more" / "See all" with instant reveal.
- **Dynamic Type XXL** — header band wraps to 4 lines; endorsements list renders one claim per visible viewport at a time; footer controls stack vertically (Follow on top, Request colleague below).
- **Right-to-left locale** (per AD-Q4) — header alignment mirrors; endorsement counts stay numerically rendered; footer button order flips per RTL action precedence.
- **Endorsement from a peer in a coop the viewer is NOT in** → endorser handle renders normally; mutual coops section is unaffected. The endorsement's existence is per-peer profile data, not per-coop data.
- **Counter-action target** (per I) — viewer who blocked-by-mistake can unblock; unblock writes a counter-action audit row but does NOT notify the formerly-blocked peer. Block + unblock both stay silent from the peer's side.
- **`[open]` Endorsement freshness decay (AE-Q5)** — should the endorsement count visually weight recent endorsements higher (e.g. ★ 12 with subtle dim on >6mo-old)? Open. Lean: dim, but keep the count integer.
- **`[open]` Peer-DM "Threads" section in state 3** — currently sketched as 13 recent thread snippets inline. Open whether this is too aggressive a surface (might leak thread context to a shoulder-surfer). Lean: behind a tap-to-reveal toggle off by default.
## Related
- [Brief AE §AE5](./AE-provider-social-network.brief.md) — profile schema source.
- [Brief AE §AE2](./AE-provider-social-network.brief.md) — connection state machine driving footer controls.
- [Brief AE §AE7](./AE-provider-social-network.brief.md) — endorsements section + endorse affordance.
- [Brief AE §AE10](./AE-provider-social-network.brief.md) — moderation menu, under-review state, block invariants.
- [Brief AE §AE11](./AE-provider-social-network.brief.md) — posture-derived rendering (states 7, 8, 9, 12).
- [`peer-directory.screen.md`](./peer-directory.screen.md) — primary entrypoint.
- [`chat-home.screen.md`](./chat-home.screen.md) — peer-DM thread row pattern reused in state 3.
- [`coop-drawer.screen.md`](./coop-drawer.screen.md) — destination of mutual-coop chip tap.
- [Brief N](./N-provider-coop.brief.md) — mutual-coop intersection source.
- [Brief K §K3](./K-safety-blocklist.brief.md) — PII gate on bio + render-time filter.
- [Brief AD](./AD-multilingual-opaque.brief.md) — bio render in viewer's locale (per AD3 re-authoring).
- [Brief I](./I-audit-trust-replay.brief.md) — every connection action + endorsement + block is an `agent_actions` row.
- [Brief V](./V-data-portability-erasure.brief.md) — erasure-vs-block indistinguishability in state 8 + 12.
- [Brief X](./X-accessibility.brief.md) — VoiceOver order + reduced motion.
- [Brief O](./O-surface-kinds.brief.md) — surface_kind vocabulary.
- [`00-system-voice.md`](./00-system-voice.md) §V2b working / §V2c plain.
## Out of scope
- The self-edit profile screen (`peer-profile-edit.screen.md` — separate file).
- The full endorsements sheet (state 3 "See all" destination).
- The endorse compose sheet (own `.screen.md` if it grows).
- Peer-DM thread interior (lives in chat-home thread variant).
- The referral-introduction compose sheet (AE1b — own `.screen.md`).
- Org-attributed peer profiles (deferred to AE-Q6 P5+).
- iPad / web variants.
## Open questions
- **Endorsement freshness decay surface treatment** (AE-Q5) — dim old endorser handles vs. visually re-rank vs. footnote a "freshness" line. `[exploratory]` (lean: dim handles older than 6mo; keep count integer; surface decay only on tap-to-detail).
- **State 3 Threads section default visibility** — show inline (current sketch) or hide behind tap-to-reveal? `[blocking]` (lean: hide-by-default to avoid shoulder-surf leak; show on tap with subtle haptic).
- **`[open]` Posture badge rendering for `incognito` peers in state 9** — currently "—" / "private". Open whether posture should be hidden entirely from incognito-peer views (so even the existence of a posture is not signaled). `[nice-to-have]` (lean: hide entirely; the mutual-coop-only stripped layout is already a posture signal in itself).