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>
17 KiB
17 KiB
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
- empty — textareas blank. Primary button disabled with label "Offer endorsement to q-berlin". Placeholder in scope-claim textarea: "Name a specific competency. ≤ 80 chars."
- 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.
- 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.
- 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. - 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_actionsper AE constraints. - sent (offered) — primary button tapped, K3 + §V6 passed, row written. Card animates off (right-slide), success haptic, transitions back to
peer-profile.screen.mdwith a working-register receipt: "Endorsement offered to q-berlin. They'll accept or pass." Sticky 30s window with↶ withdrawaffordance. - accepted-pending notification — endorsee accepted; this state is the post-state read from
peer-profile.screen.mdor 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. - withdrawn (long-press) — endorser long-pressed a previously-offered endorsement (from
peer-profile.screen.mdor the overflow's "endorsements you've offered" list) and choseWithdraw. Working-register confirm sheet: "Withdraw your endorsement for {scope}? They keep the receipt; the public surface drops." On confirm,peer_endorsements.withdrawn_atset; no notification sent to endorsee. - 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. - 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 + anEdit modechip in the header. - 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 withaccepted_at=NULL+ a mod-review flag, no public surfacing until cleared. The endorsee is not notified during the hold. - 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, writespeer_endorsementsrow withaccepted_at=NULL, public=false, animates off-right, returns topeer-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(onlyWithdrawfor accepted-public ones;Editonly 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
offeredand notifies the endorsee normally. Rejected → row stayswithdrawnand 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
incognitoposture (AE11) — endorsement can be offered but won't surface on endorsee's profile until endorser flips todiscoverableoropen. 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_idrejects; composer never opens (Endorse {handle}action is gated upstream inpeer-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 /translatedegraded — 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 withaccepted_at=NULLas usual.
Related
- Brief AE §AE7 — parent design (scope-claim shape, accept-gate, recency decay, mod-review).
- Brief AE §AE2 — colleague-state requirement.
- Brief AE §AE5 — endorsee directory profile (suggested-chip source + accept-surfacing target).
- Brief AE §AE10 — abuse-heuristic + mod-review path for state 11.
- Brief AE §AE11 — posture affecting public surfacing.
- Brief K §K3 — PII gate firing in state 5.
- Brief AD — scope-claim storage triad + endorsee-facing render.
- Brief I — every offer / accept / withdraw / mod-hold is an
agent_actionsrow. - Brief X — VoiceOver / reduced-motion / Dynamic Type behavior.
- voice §V6 — banned-phrase check fired in state 3.
- approval-card.screen.md — compose+approve sibling pattern.
- notification-rich-preview.screen.md — accept / mod-reject delivery surface.
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_operatedfirst, 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=NULLand require re-accept, or stay public with a "edited {date}" badge?[blocking](lean: revert toaccepted_at=NULLfor 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).