Builds one ProjectMember row from a host membership record and its
resolved profile fields — the ONE place the row semantics live, whichever
host (Studio, App SDK) loaded the records. The id prefers the
account-global sanityUserId; a member the host can't bridge falls back
to its project-scoped membership id, and assigning them fails loudly at
the engine's write boundary (which names the fix) rather than storing a
value that could never match. An absent optional display field leaves no key;
roles is always present, defaulting to an empty list, and each record is
narrowed to the two fields the row reads — a whitespace-only title counting as
none, so no reader downstream has to guard an empty label.
Builds one ProjectMember row from a host membership record and its resolved profile fields — the ONE place the row semantics live, whichever host (Studio, App SDK) loaded the records. The id prefers the account-global
sanityUserId; a member the host can't bridge falls back to its project-scoped membership id, and assigning them fails loudly at the engine's write boundary (which names the fix) rather than storing a value that could never match. An absent optional display field leaves no key;rolesis always present, defaulting to an empty list, and each record is narrowed to the two fields the row reads — a whitespace-only title counting as none, so no reader downstream has to guard an empty label.