cocottetech/@platform/codebase/@features/ai-copilot/docs/endorse-peer.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

17 KiB
Raw Blame History

endorse-peer.screen

Single-screen breakdown for the AE7 endorsement composer — the scope-claimed positive-reputation surface a colleague-state peer uses to offer an endorsement on a named competency (≤ 80 char) with optional evidence (≤ 200 char). Endorsements never aggregate to a global score; they are scope-claimed, recency-decayed, and accept-gated by the endorsee per AE7. Sibling to approval-card.screen (compose+approve pattern). Voice register: working by default; plain on block / banned-phrase / abuse-heuristic states (per voice §V2c — sanction copy is plain).

Layout (iPhone 17 logical 393×852, drafting — default)

┌─────────────────────────────────────────────────┐
│ ◄ status bar (system)                           │ 47pt
├─────────────────────────────────────────────────┤
│ ◄ Profile   Endorse q-berlin           [⋯]     │ 56pt — top bar
├─────────────────────────────────────────────────┤
│  q-berlin · colleague                           │ header: endorsee handle + relationship badge
│  Berlin · DE · operates: tryst, content-of      │   surface line (read-only from peer-profile)
├─────────────────────────────────────────────────┤
│                                                 │
│  ─── Scope you're endorsing ───                │
│  ╭─────────────────────────────────────────╮   │
│  │ Mandarin-speaking subject handling      │   │ scope-claim textarea (≤ 80 chars)
│  │                                         │   │
│  ╰─────────────────────────────────────────╯   │
│  44 / 80                                       │ live count (right-aligned)
│                                                 │
│  Suggested from q-berlin's surfaces:           │ chip row (single-tap fills textarea)
│  [bookings-tryst listing] [Mandarin handling]  │
│  [tour pricing · Berlin]   [PPV calendaring]   │
│                                                 │
│  ─── Evidence (optional) ───                   │
│  ╭─────────────────────────────────────────╮   │
│  │ Helped me draft three Mandarin replies  │   │ evidence textarea (≤ 200 chars)
│  │ that landed cleanly. Took 20 minutes.   │   │
│  │                                         │   │
│  ╰─────────────────────────────────────────╯   │
│  72 / 200                                      │
│                                                 │
│  Endorsements show on q-berlin's profile       │ helper text (working)
│  once they accept. Withdraw any time.          │
│                                                 │
├─────────────────────────────────────────────────┤
│  [ Cancel ]              [ Offer endorsement ] │ 56pt — action bar (primary = filled)
└─────────────────────────────────────────────────┘
                                                  │ 34pt — home indicator

Components

Component Brief ref Notes
Top bar A §navigation Back to peer-profile.screen.md; [⋯] = overflow (view existing endorsements you've offered this peer, report this peer).
Header AE7, AE2 Endorsee handle + colleague badge (right of handle, tappable to peer-profile). Sub-line: region + surface-operated chips from endorsee's directory row (per AE5).
Scope-claim textarea AE7 Free-text, ≤ 80 chars. Live count right-aligned. Placeholder rotates per state. On blur: §V6 banned-phrase check + K3 PII gate. Empty = "drafting" state.
Suggested chips AE5, AE7 Up to 4 chips derived from endorsee's peer_profiles.surfaces_operated + their public endorsements' scope-claims. Single-tap = replaces textarea content (with confirm if textarea has user-typed content).
Evidence textarea AE7 Optional, ≤ 200 chars. Same §V6 + K3 checks on blur. Helper hint: "What did they do? Be concrete."
Action bar AE7, approval-card §action-bar [Cancel] (left, secondary) + [Offer endorsement to {handle}] (right, primary filled). Primary button label dynamically includes the endorsee handle. Disabled in empty / banned-phrase-flagged / over-cap states.

States

  1. empty — textareas blank. Primary button disabled with label "Offer endorsement to q-berlin". Placeholder in scope-claim textarea: "Name a specific competency. ≤ 80 chars."
  2. drafting (default) — user is typing; live count updates; primary button enabled once scope-claim is ≥ 3 chars and not banned-phrase-flagged. No errors shown.
  3. banned-phrase-flagged — on blur, scope-claim or evidence tripped §V6 banned-phrase list (per voice §V6). Plain-register inline error directly under the offending textarea: "This phrase isn't supported here: {phrase}. Try another wording." Primary button disabled until the phrase is removed. No mention of any model or "filter" — plain noun only.
  4. over-cap — endorser has hit the per-coop endorsement cap (per AE-Q5 lean: per-coop cap + recency decay + mod review). Plain-register banner above the action bar: "You've offered the most endorsements this period for {coop name}. Try again after {date}, or withdraw an older one." Primary button disabled. [Manage your offered endorsements →] affordance.
  5. K3-gate-blocked — on send-tap, K3 PII gate detected restricted content (govt name per K3c-1, precise location per K3f-2) in either textarea. Plain-register interrupt sheet: "Held this back — contains restricted content. Edit and try again. See audit for the row." Sheet dismisses to the composer with the offending textarea focused; primary button re-enabled after edit. Counter-action row written to agent_actions per AE constraints.
  6. sent (offered) — primary button tapped, K3 + §V6 passed, row written. Card animates off (right-slide), success haptic, transitions back to peer-profile.screen.md with a working-register receipt: "Endorsement offered to q-berlin. They'll accept or pass." Sticky 30s window with ↶ withdraw affordance.
  7. accepted-pending notification — endorsee accepted; this state is the post-state read from peer-profile.screen.md or the notifications surface, surfaced back to the endorser as a working-register receipt: "q-berlin accepted your endorsement for Mandarin-speaking subject handling. It's public on their profile." Not a composer state; documented here for completeness.
  8. withdrawn (long-press) — endorser long-pressed a previously-offered endorsement (from peer-profile.screen.md or the overflow's "endorsements you've offered" list) and chose Withdraw. Working-register confirm sheet: "Withdraw your endorsement for {scope}? They keep the receipt; the public surface drops." On confirm, peer_endorsements.withdrawn_at set; no notification sent to endorsee.
  9. colleague-state-broken — endorsee withdrew colleague-state between this composer's open and send-tap (per AE2 disconnected / blocked). Plain-register sheet replaces the composer: "q-berlin is no longer a colleague. Endorsements need a colleague connection. Reconnect first." [Back] only; send-path closed.
  10. duplicate-existing — endorser already has a public (accepted) endorsement on this scope for this endorsee (scope-claim text-match, case-insensitive trim per AE7). Plain-register banner above scope-claim: "You already endorse q-berlin for Mandarin-speaking subject handling. Edit the existing one instead." Primary button label swaps to [Edit existing endorsement], route to the same composer pre-filled with the prior row's content + an Edit mode chip in the header.
  11. abuse-heuristic spike — endorsement-trading-ring heuristic (per AE-Q5) detected a spike between this endorser and endorsee (e.g. reciprocal endorsements within 24h, dense local subgraph). Plain-register banner above the action bar: "This endorsement is held for moderator review. You'll hear back within 48 hours." Primary button label swaps to [Submit for review]; on tap, row writes with accepted_at=NULL + a mod-review flag, no public surfacing until cleared. The endorsee is not notified during the hold.
  12. VoiceOver / reduced motion / Dynamic Type XXL — inherits chat-home accessibility patterns (per Brief X). Textareas read with character-count hint; suggested chips read as "suggested scope: {text}, double-tap to use"; action bar reads scope-claim + endorsee handle before reading button label.

Interactions / gestures

  • Tap a suggested chip → fills scope-claim textarea (replaces existing content with confirm if non-empty).
  • Tap scope-claim or evidence textarea → opens keyboard; live counter starts updating. On blur: §V6 + K3 gate fires.
  • Tap [Offer endorsement to {handle}] → primary action. Runs final K3 gate, writes peer_endorsements row with accepted_at=NULL, public=false, animates off-right, returns to peer-profile.screen.md.
  • Tap [Cancel] → if textareas dirty, confirm-discard sheet ("Drop this endorsement draft?"); else dismiss.
  • Long-press a previously-offered endorsement (in overflow's offered-list or on peer-profile.screen.md) → action sheet: Withdraw / Edit / View (only Withdraw for accepted-public ones; Edit only if not yet accepted).
  • Swipe-down on the composer → confirm-discard sheet (if dirty) or dismiss.
  • Tap colleague badge in header → opens peer-profile.screen.md (without losing draft — composer state persists for 60s on return).

In-the-wild copy

  • (working, primary button) "Offer endorsement to q-berlin"
  • (working, helper) "Endorsements show on q-berlin's profile once they accept. Withdraw any time."
  • (working, sent receipt) "Endorsement offered to q-berlin. They'll accept or pass."
  • (working, accepted receipt) "q-berlin accepted your endorsement for Mandarin-speaking subject handling. It's public on their profile."
  • (working, withdraw confirm) "Withdraw your endorsement for {scope}? They keep the receipt; the public surface drops."
  • (plain, §V6 banned-phrase) "This phrase isn't supported here: {phrase}. Try another wording."
  • (plain, K3 leak) "Held this back — contains restricted content. Edit and try again. See audit for the row."
  • (plain, over-cap) "You've offered the most endorsements this period for Berlin escort coop. Try again after April 28, or withdraw an older one."
  • (plain, colleague broken) "q-berlin is no longer a colleague. Endorsements need a colleague connection. Reconnect first."
  • (plain, duplicate) "You already endorse q-berlin for Mandarin-speaking subject handling. Edit the existing one instead."
  • (plain, abuse hold) "This endorsement is held for moderator review. You'll hear back within 48 hours."

Edge cases

  • Endorsee withdraws colleague-state mid-compose — composer flips to state 9 on the next foreground (subscription tick on peer_connections). Any pending K3 / §V6 checks are dropped; the row is never written.
  • Duplicate existing on same scope — state 10. Edit-mode reuses the same composer, pre-filled, with primary button label [Save endorsement]. Editing does not re-trigger accept-gate; the endorsee's prior accept persists unless the scope-claim text materially changes (per AE-Q5 recency decay logic — small edits don't re-start the clock).
  • Endorsement-trading-ring heuristic spike — state 11. The hold is silent to both parties beyond the endorser's banner; the endorsee learns nothing until mod-review clears or rejects. Cleared → row flips to offered and notifies the endorsee normally. Rejected → row stays withdrawn and the endorser gets a plain-register notice via notifications: "Your endorsement of q-berlin was not accepted by moderators. Reason category: {category}."
  • §V6 banned phrase in scope-claim only — state 3 fires under the scope-claim textarea, not the evidence one. Evidence textarea remains editable.
  • K3 leak in evidence only — state 5; sheet dismisses with the evidence textarea focused, not scope-claim.
  • Endorser is in incognito posture (AE11) — endorsement can be offered but won't surface on endorsee's profile until endorser flips to discoverable or open. Working-register helper text appears above the action bar: "You're incognito. q-berlin will see your endorsement, but it won't show on your profile to others until you change posture."
  • Endorsee blocks endorser between offer and accept — pending endorsement is silently cancelled (per AE2 block semantics); row marked withdrawn_at = now(), no notification to endorser beyond a plain-register receipt on next foreground: "Your endorsement of q-berlin can't be delivered." No mention of block status (per AE10 block opacity).
  • Endorsement against self — DB CHECK constraint endorser_id <> endorsee_id rejects; composer never opens (Endorse {handle} action is gated upstream in peer-profile.screen.md).
  • Cross-locale endorser + endorsee — scope-claim is stored canonical-EN + per-locale rendered per AD6 triad. Suggested chips show in endorser's locale; what the endorsee sees on accept is in endorsee's locale; no "translated from…" annotation ever (per AD opacity).
  • Reduced motion / VoiceOver / Dynamic Type XXL — see state 12.
  • @model-boss /translate degraded — per Brief M §M2a: scope-claim composer remains usable; AD3 register-faithful re-authoring of the endorsee-facing view queues; helper text gains a degraded chip "ai-copilot is catching up — q-berlin will see this once it's caught up." No data loss; row writes with accepted_at=NULL as usual.

Out of scope

  • The endorsee-side accept surface (lives in peer-profile.screen.md + notifications; not authored here).
  • Endorsement-trading-ring heuristic tuning (per AE-Q5; defer to P5+ with data volume).
  • Public profile rendering of accepted endorsements (lives in peer-profile.screen.md).
  • Cross-org endorsements (Quinn-as-Demimonde) — personal-only at P0 per AE-Q6 + Brief W §W4.
  • Cross-platform variants (iPad / web; deferred per Brief E).

Open questions

  • END-Q1 Suggested chips source ranking — surface endorsee's surfaces_operated first, or surface their three most-public-recently-accepted scope-claims first? [exploratory] (lean: surfaces first when endorsee has 0 public endorsements, recent-public first once they have ≥ 3 — the chip surface should evolve with the endorsee's reputation graph).
  • END-Q2 Edit-after-accept semantics — when the endorser edits an already-accepted endorsement (state 10 path with material text change), does the endorsement revert to accepted_at=NULL and require re-accept, or stay public with a "edited {date}" badge? [blocking] (lean: revert to accepted_at=NULL for material text changes (Levenshtein ≥ 0.3 on scope-claim), stay public with edit-badge for evidence-only edits; treats scope-claim as the load-bearing field).
  • END-Q3 Mod-review hold notification to endorsee — is the endorsee told that an endorsement is being reviewed (transparency), or kept fully opaque until the review clears (anti-coordination)? [blocking] (lean: opaque; the endorsee sees the endorsement only after mod-review clears, preventing the trading-ring pair from coordinating around the hold).