Person DB Resolver — a sibling remote for people, not a mode inside the org resolver
record-db-resolver only ever knew organizations — feeding it a CSV of FreedomFest speakers wrote a person into the organizations table and minted a nonsense opportunity. The fix is a new remote, person-db-resolver: match-or-create a person, then independently match-or-create their org and RELATE the affiliation with a role. Person and org are separate decisions the operator can make in either order, together or alone — not a coupled 1:1.
Why Care?
Running the FreedomFest 2026 speaker CSV through record-db-resolver —
built to be the generic "reconcile a record to a canonical org" bridge —
surfaced that "generic" was never true. It matched "Lyn Ulbricht" as a
fuzzy organization name, and clicking "create new org from this record"
on Ethan Akimoto actually created an organizations row named
ethan-akimoto plus an opportunity, both of which had to be deleted from
SurrealDB Cloud by hand. Two disconnected systems already wrote to the
canonical layer — the shell-reachable resolver.* capabilities (org-only)
and standalone CLI scripts (surreal-write-persons.mjs,
surreal-write-event-attendees.mjs — person-aware, but unreachable from
any UI) — and neither was both person-aware and shell-reachable.
What's New?
person-resolver.ts(new, sibling toresolver.ts) —person.candidates(match bylinkedin_profile_urlfirst, else fuzzy name + org-in-headline boost),person.apply(create/match, idempotent, backfills aperson_uuidon pre-existing rows from before this capability existed),person.affiliate(resolve the org — reusingresolver.ts's match/create logic via a newly-extractedresolveOrgRowhelper, not reimplemented — thenRELATEthe affiliation with a role and write the observations), andperson.add_observation(a free-form manual observation on top of whatever got auto-derived).RecordId round-trips fixed the way
domains.tsalready learned this lesson once (source_uuidcast to string) — every id that crosses the NATS/WS wire and comes back in a later call is a plainperson_uuidstring, re-looked-up fresh inside the query that needs a real RecordId. The very first version of this shipped without that and failedperson.affiliatewith aRELATEtype error on the first live test.Person and org are independent, not sequential — the actual design point, and the first shipped version got it wrong: the org section was gated behind
personResult, contradicting the "mutually independent OR interdependent, operator's choice" decision from earlier in the conversation. Fixed:person.affiliate'sperson_uuidis now optional — org-only resolution (no person, no affiliation edge) is a first-class outcome, and if a person happens to already be resolved on the same row, the affiliation gets RELATEd in the same call.apps/person-db-resolver(new remote, port 3010) — same record-set picker / back-skip-next chrome asrecord-db-resolver, but: a column-mapping step asked once per record set (which column is the name? org? role? LinkedIn URL? observation?), persisted and silently reused — the direct fix forrecord-db-resolver's bug of hardcoding five Master Pipeline Tracker column names and dropping everything else a differently-shaped CSV carried. Then: person candidates (match/create/skip — skip is expected, not a failure), independently an org name input + candidates (match/create), and a manual predicate/value observation form.record-db-resolveralso got two fixes discovered along the way:RecordCardnow shows every raw CSV column, not just the org-normalized subset; and it was missing theactive-record-set-changedlistenerpack-runneralready had, so a click from Record Collector on an already-mounted resolver landed on an empty "pick a record set" picker instead of the uploaded set.record-collectorgained a second button — "Resolve Orgs to Canonical DB" (renamed from the single "Resolve to Canonical DB") next to a new "Resolve People to Canonical DB", and a new "Augment a CSV of People" flow (PEOPLE_ROTATION: recordCollector → personDbResolver), sibling to the org-shaped "Augment a CSV of Event Attendees."
Also included
context-v/plans/Person-Aware-Canonical-Resolver-Extension.md— the design doc this was built from, including the two decisions made mid-build (separate remote, not a mode; per-record-set column mapping) and the open question this leaves: an org resolved before its person is resolved doesn't retroactively RELATE if the person gets resolved afterward on the same row — resolve person first for the automatic link, or re-run the org step after, until that's worth closing.Both new backend capabilities were smoke-tested end to end against real FreedomFest data (Ethan Akimoto → person created, Carl Menger Institute matched/created, affiliation + observations written, idempotent on re-apply) before any UI was built around them.
