ADR-0101 Adversarial Review — Final Report

1. TL;DR

The panel unanimously rejects the incumbent ADR-0101 and recommends a hybrid built on the conversation-link spine: put the on-behalf relationship on the visitor's own Conversation (onBehalfOf), not as a field on the principal's inquiry — the only shape that makes pollution-suppression, the "arrived" alert, and the concurrency story work by construction rather than by prompt-hope or an empty lookup. It borrows the role-discriminant enum and reasoning-based candidate-injection recognizer that all three reframe designs converged on (best articulated in generic-party), the process-tour ordering guard and honest Temporal reminder plumbing from tour-centric, and a corroboration gate on any linkage (from the tour-centric skeptic) as the fair-housing rail. Two requirements survive every option and are non-negotiable: a locked byte-identical PM subject string (people don't read bodies), and the voice/relocator path must be plumbed or scoped out loudly. The founder's "hacky / not generic enough" instinct is fully vindicated — the fix is a small generic "party acting on behalf" concept, not a bespoke expectedVisitor patch and not a platform.

2. Scoreboard (judge-averaged, n=3; weighted totals: Carly fit ×2, genericity ×1.5, recognition ×1.5, PM clarity ×1.5, compliance ×1.5, impl cost ×1 — max 90)

Option Carly fit Genericity Recognition PM clarity Impl cost Compliance Total
conversation-link 7.0 8.0 7.2 7.8 6.0 6.8 64.75
generic-party 5.7 8.0 7.2 7.2 4.7 5.2 57.25
tour-centric 5.7 6.3 7.2 6.2 4.2 3.8 50.75
ADR-0101 incumbent 3.3 2.0 2.2 1.2 5.8 4.2 26.75

Judge disagreements >2 points (all driven by Judge 3, compliance-ops, weighting compliance + honest impl-cost harder):

3. What broke ADR-0101 under attack

Critiques that SURVIVED cross-examination (fatal to the incumbent):

Critiques that DIED / were downgraded under cross-examination (do not carry into the build):

Spine: conversation-link. The on-behalf relationship is a property of the visitor's conversation, written once from that conversation (single-writer, same stampConversationSubtopicupdateItemFields path as the existing tenantMatchType/subtopic markers). No INQUIRY_C_FIELDS dual-write, no GSI, no list-merge concurrency machinery. This is the piece that dissolves the lost-update race and hands the arrived-alert a keyed reverse pointer for free.

Schema

(a) Conversation.onBehalfOf — the linkage of record (from conversation-link):

onBehalfOf?: {
  inquiryId: string | null;          // principal's ProspectInquiry; null in representative-first cases
  principalPersonId?: string;
  principalLabel: string;            // "Carly" / "my daughter Maya" / ''
  role: 'visitor' | 'co_applicant' | 'representative' | 'guarantor' | 'support';
  status: 'pending' | 'active' | 'fulfilled' | 'expired';  // soft 30d TTL at read
  linkedAt: string;
  note: string;
};

No phone/email for anyone → nothing here can trigger outbound (TCPA preserved by construction).

(b) ProspectInquiry.expectedParties — advisory hint array, write-once from the principal's own thread only, never mutated cross-conversation (this is what kills the race). Read only to tighten the recognition candidate list. Role enum borrowed from generic-party.

(c) Tour.onBehalfOf — a render cache populated from the resolved conversation link (not a fuzzy match), so every PM surface renders "Visitor (for Principal)" without a second lookup and the tour-day alert resolves the visitor by back-reference. Replaces ADR-0101's Tour.onBehalfVisitor.

Role discriminant — borrowed from generic-party/tour-centric

visitor | co_applicant | representative | guarantor | support. Drop co_applicant from Phase 1 (J2 + J3 lean): co-applicants belong to the application, not the tour-attendee concept, and excluding them from suppression reopens the Appendix-B couples double-count. Keep the enum value reserved so the schema is future-ready.

Recognition — reasoning-based, corroboration-gated

Honest residual to name for the founder: NO option achieves a hard code gate on save_prospect at the visitor's first inbound inside the agent loop. The corroboration-gated conversation-link read is the backstop and closes the deterministic process-tour path; the in-loop save_prospect remains prompt-influenced. This is a real ceiling, not a bug we're hiding.

PM communication — locked, subject-first

One byte-identical canonical subject string reused across all three PM surfaces (send-pm-tour-confirmation.ts, the Outlook event event-copy.ts, the arrival alert):

Body: prospect card shows "Jordan, visiting for Carly"; Carly stays the deal contact. The PM understands same inquiry, third party at the door from the subject alone.

Temporal reminders — priced honestly (from tour-centric)

Not "one seam." Reminders must reach the person at the door: attendeePersonId workflow input + all three activities (confirmation, T-24h, T-1h) resolved off the visitor's conversation channel (which may be email, not SMS). The loop-close follow-up (D4) goes to the principal and must fork the payload off the existing post-tour app-link timer (Carly said she isn't ready — no application talk).

Voice

The candidate-injection must be threaded into the voice/ElevenLabs prompt assembly, a different path from the text agent loop, left unverified by every option. The canonical out-of-town relocator usually calls. Decision required (see §5) — plumb it or scope it out loudly; do not claim coverage.

Phased delivery

5. Open decisions only the founder can make

  1. Voice recognition — plumb in Phase 2, or scope out of v1 and ship text/email only? Panel lean: must be explicit either way; given the relocator usually calls, plumb it in Phase 2 rather than ship a hole the canonical case falls through — but that's a cost the founder should accept knowingly.
  2. Keep the expectedParties hint on the inquiry (warm "we were expecting you"), or zero inquiry change (cold-but-polite "who are you here for?")? Panel lean: keep it — write-once from the principal's own thread carries no race, and it's the difference between warm recognition and an interrogating question.
  3. co_applicant role — build now, or reserve the enum value and defer? Panel lean: defer (J2 + J3). Co-applicants belong to the application; including them in the tour-attendee suppression reopens the couples funnel-pollution hole.
  4. Representative-first direction (parent-for-student, assistant-for-exec — inquiryId: null) — build now or schema-ready-only? Panel lean: schema-ready, out of scope for the build. The nullable link makes it representable without new fields; ship it when a real case lands, and state in the ADR that it's explicitly not wired yet rather than implying expectedVisitor ever covered it.