# Campaign Member Lead Lookup Search

## Problem Statement

Marketing users can open the Campaign Member create form from a Campaign, but the
Lead lookup field does not return any results for name or email searches. Issue
#2027 reports this on staging build v17795475, tested on 2026-05-27. The same
form's Contact lookup returns expected matches, so the user-facing failure is
not "lookup search is broken everywhere"; it is specifically that Campaign
Member creation cannot discover Leads through the standard form workflow.

This blocks Story #763 AC1. A marketing user should be able to add either a
Lead or a Contact to a Campaign, search by partial name, full name, or email,
select the person, save the Campaign Member, and see the relationship persist.

## Salesforce-Compatible Implementation Understanding

Salesforce models `CampaignMember` as the record that connects a `Campaign` to
the person who participated in it. For this path, the person is represented by
either `LeadId` or `ContactId`. Creating a Campaign Member therefore requires a
Campaign and one selected Lead or Contact, and the lookup picker is not a
cosmetic field: it is the main user workflow for choosing the participant.

Lead and Contact use a Salesforce-style compound `Name`. Users and imports set
constituent fields such as first name, last name, salutation, middle name, and
suffix. The display/search field `Name` is the synthesized person name. A
Salesforce-compatible implementation must therefore preserve two facts at once:

- the form must search person records by ordinary lookup terms such as name and
  email
- name display must work even when the stored data is represented through
  compound name components rather than a user-editable `Name` field

The Salesforce-shaped search contract for this UI is a single-object SOSL-style
lookup search. The frontend builds a search request for the target object,
usually `FIND {term} IN ALL FIELDS RETURNING Lead(Id, Name)` or the Contact
equivalent. The backend expands `ALL FIELDS` through metadata into searchable
fields, applies object and field permissions, applies row visibility, returns a
small set of fields for display, and normalizes `Name` when the object uses
compound name metadata.

Scoped lookup context is only part of the contract when the source lookup field
declares runtime lookup filtering. `CampaignMember.Status` is runtime scoped by
Campaign in the current pack, but `CampaignMember.LeadId` and
`CampaignMember.ContactId` are plain reference fields. A Lead lookup failure on
this form should not be explained by Campaign-scoped picklist behavior unless
the installed metadata has drifted from the tracked final pack.

The current ownership boundary is important:

- generic SOSL, lookup search, permissions, compound-name synthesis, and lookup
  hydration belong upstream in NexusCRM
- generic Salesforce standard-object metadata belongs in the official standard
  CRM app pack
- PingCAP-specific overlays and final pack assembly belong in the downstream
  PingCAP pack

## Status Quo

Issue #2027 is open and reports that any Lead lookup query on the New Campaign
Member form returns "No results." The reproduction explicitly covers both Lead
name fragments and Lead email fragments. The same issue states that Contact
lookup works on the same form.

Story #763 has earlier history for the same journey. On 2026-05-07, build
v17781251 recorded AC1 as passing for Lead lookup. On 2026-05-11, build
v17784580 recorded AC1 as failing again, with a hypothesis around Lead `Name`
synthesis. Issue #2027 later shows the regression still present on build
v17795475.

The legacy upstream fix PR for the earlier regression made the intended corridor
clear. It allowed Salesforce-style compound-name creates to satisfy required
field preflight, synthesized `Name` on insert and update, widened Lead list
search beyond only `Name`, narrowed SOSL field expansion to avoid TiDB recursion
failures, and verified that Campaign Member Lead lookup returned a created Lead.

The current downstream final pack metadata looks structurally correct for the
reported workflow:

- `CampaignMember.LeadId` references `Lead`
- `CampaignMember.ContactId` references `Contact`
- neither lookup field declares runtime-scoped lookup filtering
- installed/runtime metadata must have Lead searchable; the current source spec
  does not disable search, and upstream installs pack objects as searchable
  unless explicitly disabled
- `Lead` has a `Name` field and compound name member metadata
- Lead list/search display fields include `Name`, `Company`, `Phone`, `Email`,
  `Status`, and `OwnerId`
- `Contact` follows the same person-record pattern

The current upstream runtime also appears to implement the expected generic
behavior. The searchable lookup component sends a single-object SOSL request
with `Name` as the display return field. The REST search path expands `ALL
FIELDS` to include the name field, compound name members, list fields, and other
metadata-searchable fields. Query execution prunes unreadable fields and returns
only visible rows. Record persistence synthesizes compound `Name` values on
insert and update from field provenance. Lookup hydration falls back through
record reads when the search result does not already carry a display name.

That means the tracked source tree does not currently point to a simple
downstream metadata defect. The leading status-quo possibilities are:

1. The staging build or installed metadata behind v17795475 was not equivalent
   to the current tracked upstream runtime plus current final pack.
2. Installed metadata drifted after deployment, such as stale Lead field
   provenance, stale `CampaignMember.LeadId` validator metadata, or missing Lead
   search/list fields.
3. The marketing test user has Lead object or field permission differences that
   leave no readable Lead fields for SOSL search, while Contact remains
   searchable.
4. Historical Lead records in staging have blank stored `Name` values from an
   older import/runtime, and the deployed search path is only searching `Name`
   rather than current `ALL FIELDS` expansion.
5. The browser is sending the wrong lookup target or an unintended lookup
   context for `CampaignMember.LeadId`.

The coverage gap is also real. The existing Campaign Member API watchdog proves
that a Lead can be created and associated to a Campaign Member by Id, but it
does not call the lookup search endpoint. Browser coverage opens Campaign
surfaces but does not assert that the Lead lookup dropdown can find a seeded
Lead by name and email. This regression escaped because the user journey was
not pinned at the search seam.

## Solution

Close #2027 by making Campaign Member Lead lookup search a source-backed,
tested contract instead of a manually verified form behavior.

First, add diagnostics and tests that distinguish the failure layer. The
assertions that matter must run as the affected Campaign actor,
`marketing-user`, because the reported defect is a marketing Campaign workflow.
Setup may run under an admin or fixture-writing actor when needed, but the
readability controls, describe assertions, lookup SOSL calls, and browser form
assertions must use the same `marketing-user` token/session. Before each lookup
SOSL assertion, the case must prove that the same actor can read the seeded Lead
and Contact through a direct read or SOQL positive control. A failure should
identify whether the problem is final pack metadata, installed metadata drift,
runtime search/permissions, actor permissions, or frontend lookup wiring.

Second, fix the failing layer only after the diagnostic seam identifies it. If
the REST lookup search returns the Lead for name and email, but the browser
dropdown still shows no results, the fix belongs in frontend lookup target,
context, or hydration logic. If REST lookup search fails while SOQL can read the
Lead, the fix belongs in upstream search, field-level security filtering,
compound-name handling, or installed metadata. If the final pack audit fails,
the fix belongs in the official standard CRM app pack or the downstream overlay,
depending on whether the missing fact is generic Salesforce CRM metadata or
PingCAP-specific assembly.

Third, promote the successful behavior into deployment readiness. A staged
environment should not be considered ready if the final installed pack says
`CampaignMember.LeadId` is not a plain Lead reference, if Lead is not searchable
by name and email through the lookup search endpoint, or if the Campaign Member
browser workflow cannot select a seeded Lead.

## Canonical Behavior Contract

| Surface | Required behavior |
| --- | --- |
| Campaign Member describe | `LeadId` targets Lead, `ContactId` targets Contact, and neither field has unintended runtime-scoped lookup filtering |
| Lead describe | Installed metadata marks Lead searchable for the lookup path, exposes readable name metadata, and exposes email as a searchable/readable lookup search candidate for `marketing-user` |
| Lead compound name | Lead `Name` is synthesized from person-name components on create and update, and lookup display can render `Name` without requiring users to edit it directly |
| Lead name lookup | A partial or full Lead name term returns matching visible Leads through the same single-object search path used by the lookup component |
| Lead email lookup | A Lead email substring returns matching visible Leads through the same single-object search path used by the lookup component |
| Contact parity | Contact name and email lookup continue to work on the same Campaign Member form |
| Permissions | Empty Lead results are valid only when `marketing-user` cannot read the Lead object or any searchable Lead fields needed for the query; otherwise the seeded same-token read positive control must pass before SOSL is evaluated |
| Browser form | The New Campaign Member Lead lookup shows seeded Lead results, allows selection, saves the Campaign Member, and persists `LeadId` |
| Drift detection | Installed metadata drift from the tracked final pack is observable before release rather than discovered by manual UAT |

## User Stories

1. As a marketing user, I want to search for an existing Lead by partial name on
   the New Campaign Member form, so that I can add that Lead to a Campaign.

2. As a marketing user, I want to search for an existing Lead by full name, so
   that exact-name lookup works when I already know who I am adding.

3. As a marketing user, I want to search for an existing Lead by email fragment,
   so that I can find the right person when names are ambiguous.

4. As a marketing user, I want Contact lookup to keep working on the same form,
   so that fixing Lead lookup does not regress the parallel participant path.

5. As a marketing user, I want a selected Lead to remain selected after the
   lookup closes, so that I can finish the Campaign Member form confidently.

6. As a marketing user, I want the saved Campaign Member to persist the selected
   Lead, so that the Campaign's member list reflects the person I chose.

7. As a marketing user, I want a clear empty state only when there are truly no
   visible matching Leads, so that I do not mistake a system failure for a data
   problem.

8. As a marketing user, I want lookup results to show a readable person name, so
   that I can distinguish between multiple matching Leads.

9. As a marketing user, I want lookup results to remain permission-safe, so that
   I only see Leads and fields I am allowed to read.

10. As a sales operations admin, I want `CampaignMember.LeadId` to be represented
    as a standard Lead reference, so that lookup behavior follows generic CRM
    metadata rather than a special-case frontend branch.

11. As a sales operations admin, I want Campaign Member status scoping to remain
    independent from Lead lookup search, so that Campaign-specific statuses do
    not accidentally suppress participant search.

12. As a pack maintainer, I want Lead lookup search behavior to be proven against
    the generated final pack, so that local source metadata and installed
    staging metadata cannot silently diverge.

13. As a pack maintainer, I want generic Salesforce CRM metadata to live in the
    official standard CRM app pack when possible, so that PingCAP overlays do
    not permanently own standard CRM facts.

14. As an upstream runtime maintainer, I want lookup search to use the generic
    SOSL and permission pipeline, so that Lead lookup does not require a
    hardcoded object-specific escape hatch.

15. As an upstream runtime maintainer, I want compound-name synthesis to cover
    Lead and Contact consistently, so that person-record lookup display is not
    dependent on stale stored `Name` values.

16. As a QA owner, I want an API watchdog case for Campaign Member Lead lookup
    search, so that the search seam fails before browser UAT.

17. As a QA owner, I want a browser watchdog case for the full Campaign Member
    Lead lookup workflow, so that a passing API create-by-Id test cannot hide a
    broken user picker.

18. As a release owner, I want readiness checks to identify whether a failure is
    pack metadata, installed metadata drift, runtime search, permissions, or
    frontend lookup wiring, so that regressions can be routed to the right owner
    quickly.

19. As a release owner, I want #2027 to close only after the tested staging build
    proves name and email search for Lead, so that the issue is not resolved by
    inspecting source alone.

20. As a migration operator, I want historical Leads with incomplete stored
    display names to remain findable by email when the user can read email, so
    that old data does not break Campaign Member creation.

## Implementation Decisions

- Treat #2027 as a contract and diagnostic problem first. The current source
  tree already contains the expected Lead reference, Lead compound-name metadata,
  Lead list fields, and generic lookup search runtime. The implementation must
  prove the failing layer before changing behavior.

- Use the highest external behavior seam available: installed-pack REST search
  plus the browser form workflow. Unit tests are appropriate only for the layer
  that diagnostics identify as defective.

- Pin diagnostic assertions to the affected actor. The new API and browser
  checks must run the Lead/Contact readability positive controls and lookup
  search assertions as `marketing-user`; setup can use a different actor only
  when the assertions switch back to the marketing session before verification.

- Keep lookup search generic. Do not add a Lead-only frontend registry, a
  Campaign Member Lead special case, or a hardcoded email-field search branch
  unless the generic metadata-driven path is proven insufficient.

- Keep Campaign Member status scoping separate from participant lookup search.
  `Status` can remain Campaign-scoped while `LeadId` and `ContactId` remain
  ordinary references.

- Preserve the upstream/downstream ownership boundary. Runtime search,
  permissions, compound-name synthesis, and lookup hydration fixes belong
  upstream. Generic Salesforce standard-object metadata belongs in the standard
  CRM app pack. PingCAP-specific pack assembly and overlay corrections belong
  downstream.

- Treat final pack metadata as the release artifact. Any pack change must be
  made in the owning source pack or overlay and then regenerated into the final
  pack; do not hand-edit the generated final pack.

- Treat installed metadata drift as a deploy problem, not a reason to encode
  staging-specific behavior in source. If staging metadata differs from the
  tracked final pack, repair deployment or seed/application flow and add a
  readiness check.

- When diagnosing permission behavior, distinguish "the Lead object is not
  visible," "Lead is visible but all searched fields are hidden," and "fields
  are visible but the search implementation chose the wrong field set." All
  three can produce empty lookup results but have different fixes.

- Do not require a historical backfill of every blank Lead `Name` unless current
  lookup search cannot satisfy name and email search through metadata and
  compound synthesis. If backfill is needed, make it a separate data-repair
  decision with an explicit migration/runbook.

- Close the issue only on a build that proves both Lead name search and Lead
  email search from the Campaign Member form. Contact parity is a regression
  guard, not a substitute for Lead proof.

## Testing Decisions

- A good test for this PRD verifies external user-visible behavior or API
  contracts, not implementation details. It should fail when a marketing user
  sees "No results" for a seeded visible Lead and should pass regardless of the
  internal field-expansion implementation chosen to satisfy the contract.

- Add an API watchdog case for Campaign Member participant lookup. The case
  should create or seed a deterministic Campaign, Lead, and Contact, then run
  the user-facing assertions as `marketing-user`. Under that same token, assert
  describe metadata for `LeadId` and `ContactId`, prove direct readability of
  the seeded Lead and Contact through SOQL or record read positive controls,
  query Lead lookup search by partial name, full name, and email fragment, query
  Contact lookup search as a parity guard, and create a Campaign Member with the
  returned Lead id.

- Add a browser watchdog case for the Campaign detail to New Campaign Member
  workflow. The case should open a Campaign, start a new Campaign Member, search
  the Lead lookup by name and email, verify the seeded Lead appears, select it,
  save, and verify the Campaign Member is associated with the Lead.

- Add or extend final-pack metadata audit coverage so build verification fails
  when Campaign Member participant references, Lead searchability, Lead email
  list/search metadata, or Lead compound-name metadata disappear from the final
  pack.

- If diagnostics point to upstream search expansion, cover it with SOSL field
  selection tests that prove `IN ALL FIELDS` includes readable Lead name,
  compound-name, list, and email fields without reintroducing broad recursion
  over every field type.

- If diagnostics point to upstream permission pruning, cover it with query
  service tests that prove Lead lookup returns results when the user can read at
  least one matching searchable field and returns empty only when permissions
  require it.

- If diagnostics point to compound-name synthesis, cover insert, update, and
  lookup display paths for Lead and Contact person-name components.

- If diagnostics point to frontend lookup wiring, cover target object,
  lookup-context construction, minimum search term behavior, result hydration,
  and display-label fallback for `CampaignMember.LeadId`.

- Keep the existing Campaign Member create-by-Id API case. It proves persistence
  but is lower-level than this regression and must not be treated as sufficient
  coverage for #2027.

- Run the final validation against the same artifact shape used by deployment:
  current upstream NexusCRM, the official standard CRM app pack, the PingCAP
  overlay, and the regenerated final pack.

## Out of Scope

- Reworking Campaign Member status picklist scoping.

- Solving duplicate Campaign Member prevention, `HasResponded`, response dates,
  or converted-Lead semantics beyond avoiding regressions.

- Redesigning the Campaign Member create form or replacing the generic lookup
  component.

- Adding a global search page or changing non-lookup search UX.

- Shipping a staging-only hot patch outside the source-backed pack/deploy
  contract.

- Backfilling all historical Lead names unless diagnostics prove current
  metadata-driven lookup search cannot satisfy the issue without data repair.

- Changing Contact lookup behavior except as a parity guard.

## Further Notes

The most important design constraint is that the current tracked source already
appears to contain the expected metadata and generic runtime behavior. The PRD
therefore avoids prescribing a blind fix. It prescribes the contract, the
diagnostic seams, and the ownership rules that determine where the actual patch
belongs.

The relevant historical breadcrumb is the legacy fix PR that verified the full
Lead corridor: compound-name create, stored/display `Name`, SOSL search field
selection, lookup hydration, and Campaign Member Lead lookup. #2027 should be
closed only when that corridor is proven again on the current final-pack
deployment.
