Skip to content

Commit dafe2a6

Browse files
committed
init(microfe, microservice, affiliations): initiating new mfe ms for augmenting from affiliations as starting point, for use case of event freedomfest
Every prior augment-it flow starts from an uploaded CSV and resolves it into the canonical layer. This spec is the first flow that runs the other direction: the canonical layer already has the data (65 FreedomFest 2026 speakers, 61 of them RELATEd to an organization), and the job is to augment what's already there — add links, attach corpus content, and rate relevance — not resolve new records in. The design splits into two independent loops rather than one new worklist app. Relevance rating (Very Relevant / Highly Relevant / Relevant / Skip / Irrelevant, plus a why-note) round-trips through a CSV: export via an extension of the existing export-event-attendees-csv.mjs script, edit offline in a spreadsheet, reimport through Record Collector's existing upload path, resolve with one new capability. Links and corpus content reuse person-enrichment's existing per-affiliation surface as-is, with its hardcoded event slug turned into a picker — no new app needed for that half. Key data-model decision: relevance lives on the affiliations RELATE edge itself, not on persons/organizations (both multi-tenant — a bare field there would leak one client's ratings to another) and not a new opportunities-like table (confirmed org-only and CSV-record-coupled, not a fit for an affiliation-driven origin). Files changed: - context-v/specs/Augment-From-Affiliations.md (new)
1 parent ede29e8 commit dafe2a6

1 file changed

Lines changed: 280 additions & 0 deletions

File tree

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
---
2+
title: "Augment from Affiliations — the first flow that starts from SurrealDB instead of a CSV, turning an event's speaker/org pairs into a rated, sourced prospect list"
3+
lede: "Every flow in augment-it so far starts with a CSV — upload, map columns, resolve. This one starts with data already in the canonical layer: pick an event, export every person↔org [[Client-Tagging-on-Canonical-Writes|affiliations]] edge tied to it as a CSV, rate relevance offline in a spreadsheet, reimport — while links and corpus content get added through the existing per-affiliation surface, not a new one. Building the sourced short-list a Reach.Edu team member needs to walk into FreedomFest 2026 and know who's worth a conversation."
4+
date_created: 2026-07-07
5+
date_modified: 2026-07-07
6+
authors:
7+
- Michael Staton
8+
augmented_with:
9+
- Claude Code on Claude Sonnet 5
10+
semantic_version: 0.0.0.2
11+
revisions:
12+
- 2026-07-07 — v0.0.0.2: split into a hybrid — relevance rating moves to a CSV export/reimport round-trip (bulk-editable, reuses Record Collector's existing upload path); links + corpus point at `person-enrichment`'s existing per-affiliation surface (de-hardcoded from one event) instead of a new worklist UI. Smaller build, more reuse, per operator direction.
13+
- 2026-07-07 — v0.0.0.1: initial draft — single new all-DB-native worklist app covering rating + links + corpus together.
14+
status: Draft
15+
tags:
16+
- Spec
17+
- Augment-It
18+
- Affiliations
19+
- Canonical-Layer
20+
- Relevance-Rating
21+
- Person-Enrichment
22+
- Org-Resolution
23+
- CSV-Round-Trip
24+
- Reach-Edu
25+
- FreedomFest
26+
---
27+
28+
# Augment from Affiliations
29+
30+
## Why this exists
31+
32+
The CEO of Reach.Edu is speaking at FreedomFest 2026. Between now and then,
33+
someone on the team needs a working list: who else is speaking or attending
34+
who's worth a conversation about philanthropic fundraising, what
35+
organization connects them, and enough context on each to make the
36+
approach make sense. The raw material already exists — 65 speakers
37+
resolved into `persons`, their organizations resolved into `organizations`,
38+
61 of them now `RELATE`d by an `affiliations` edge with a role — but
39+
nothing rates who actually matters, and nothing packages that judgment
40+
into something shareable.
41+
42+
Every prior augment-it flow (`record-db-resolver`, `person-db-resolver`,
43+
[[Sparse-Person-Enrichment-Surface]]) starts from an uploaded CSV and
44+
resolves it into the canonical layer. This is the first flow that runs the
45+
other direction: the canonical layer already has the data, and the flow's
46+
job is to **augment what's already there**, not resolve new records in.
47+
48+
**v0.0.0.2 revision:** the first draft designed one new worklist UI to
49+
cover rating + links + corpus together. Reconsidered — those are two
50+
different shapes of work, and forcing them into one surface meant building
51+
more than either needed. Rating 61 rows is a bulk, spreadsheet-shaped task;
52+
adding links and corpus content is a one-at-a-time, already-solved task.
53+
Splitting them let most of this spec become "point at what already exists"
54+
instead of "build a new app."
55+
56+
## User Flow
57+
58+
Two separate loops now, not one. Both operate on the same underlying data
59+
(the affiliation edge and the person/org it connects), just through
60+
different surfaces suited to the shape of each task.
61+
62+
### A. Rate relevance — export, edit offline, reimport
63+
64+
1. **Export.** Run the (extended) `scripts/export-event-attendees-csv.mjs`
65+
for the picked event — it already produces one row per attendee with
66+
their affiliation (org + role), links, and corpus URLs pulled straight
67+
from canonical. Add two columns: `relevance` (blank, or pre-filled if
68+
already rated) and `relevance_note` (blank). Add two more columns that
69+
exist for reimport, not for editing: `person_uuid` and `org_slug` — the
70+
pair that identifies exactly which `affiliations` edge a row belongs to.
71+
72+
2. **Edit offline.** The operator opens the CSV in whatever spreadsheet
73+
tool they already use. Sort, filter, fill down, work in whatever order
74+
makes sense, share the file with a colleague if a second opinion helps
75+
— none of that needs to be built, a spreadsheet already does it. Fill
76+
in `relevance` (one of `Very Relevant` / `Highly Relevant` / `Relevant`
77+
/ `Skip` / `Irrelevant` — see [Data Model Decisions](#data-model-decisions)
78+
for what each means) and, ideally, `relevance_note` — the eventual
79+
export needs to say more than a label; "Whole Foods founder, active in
80+
liberty-movement philanthropy circles, worth a warm intro via X" is
81+
what actually helps the CEO walk into a conversation prepared. Leave a
82+
row blank to skip it entirely — that's different from rating it
83+
`Skip`, see below.
84+
85+
3. **Reimport.** Upload the edited CSV through the existing Record
86+
Collector path — same generic upload this app already has, no changes
87+
needed there (columns are always derived from headers, never
88+
predefined, per this app's standing convention). This creates a
89+
record-set like any other upload.
90+
91+
4. **Resolve.** A new small resolver step (`affiliation-rating-resolver`,
92+
details below) works through that record-set's rows: for each, look up
93+
the `affiliations` edge by `(person_uuid, org_slug)`, and if `relevance`
94+
is non-blank, write it via a new `affiliation.rate` capability. Same
95+
per-record-set **column mapping** step [[Sparse-Person-Enrichment-Surface]]'s
96+
sibling flows already use — the operator confirms once which columns
97+
are which, not re-typed per row. Rows with blank `relevance` are
98+
no-ops. Rows whose `relevance` value doesn't match one of the five
99+
allowed values are **flagged, not silently coerced or dropped** — same
100+
discipline as everything else touching this canonical layer.
101+
102+
### B. Add links and corpus content — the existing per-affiliation surface
103+
104+
`apps/person-enrichment`'s `AffiliationCard` already does this — an
105+
operator working one affiliation at a time can add a link (website,
106+
LinkedIn profile, LinkedIn company page, X, blog, Substack, YouTube, etc.
107+
— auto-detected from whatever URL they paste) or a corpus entry (a press
108+
mention, an interview, a bio page — content *about* the person or org,
109+
not a canonical profile link) to the person side, the org side, or both.
110+
111+
The one real gap: `person-enrichment` currently hardcodes
112+
`EVENT_SLUG = '2026-05-21-turning-jobs-into-degrees'`. Turning that into
113+
the same event-picker described in step A.1 (a dropdown of `events` rows,
114+
client-scoped) is the actual new work here — everything downstream of the
115+
picker already exists and needs no changes.
116+
117+
### How the two loops relate
118+
119+
Independent and can happen in either order, on different days, by
120+
different people. Rating doesn't need links/corpus to exist first, and
121+
vice versa — the export in step A.1 shows whatever links/corpus already
122+
exist at export time as read context, but doesn't require them.
123+
124+
## Data Model Decisions
125+
126+
### Relevance lives on the `affiliations` edge, not on the person or the org
127+
128+
Considered and rejected: a new table (the user's own first instinct was
129+
"maybe like `opportunities`?"). Checked — `opportunities` is confirmed
130+
org-only (no person field anywhere in its schema) and keyed 1:1 to a
131+
`record_uuid` from a CSV-driven resolution, which this flow's *data*
132+
doesn't have (only the *rating pass* now round-trips through a CSV — the
133+
affiliation itself was resolved earlier, by `person-db-resolver`).
134+
Bending `opportunities` to fit would mean stripping out the one thing
135+
that makes it `opportunities` — not worth it for a rating.
136+
137+
Considered and rejected: a field directly on `persons` or `organizations`.
138+
Both tables are multi-tenant (`client_access: string[]` — the same org row
139+
can be visible to reach-edu *and* humain-vc). A single `relevance` field
140+
on the row would leak one client's private prioritization to every other
141+
client who can see that org, or get silently overwritten when two clients
142+
rate the same row differently. Real correctness bug, not a hypothetical.
143+
144+
**Decision:** add `relevance: string | null` and `relevance_note: string |
145+
null` directly to the `affiliations` `RELATE` edge, alongside the
146+
`kind`/`client_access`/`added_at` fields it already carries
147+
(`person-resolver.ts`'s `applyPersonAffiliation`, current shape). The edge
148+
is already client-scoped, already uniquely identifies "this person, this
149+
org, in this relationship," and is exactly what the CSV round-trip's
150+
`(person_uuid, org_slug)` key resolves to. No new table, no leak risk.
151+
152+
Also add `relevance_rated_by` / `relevance_rated_at` — this app's standing
153+
actor-attribution pattern (every mutation carries who did it) extends here
154+
the same way it does everywhere else.
155+
156+
**The five values, precisely:** `Very Relevant` / `Highly Relevant` /
157+
`Relevant` / `Skip` / `Irrelevant`. `Skip` is a genuine stored rating —
158+
"reviewed, not worth pursuing *right now*, may revisit" — not the same
159+
thing as a row the operator never got to (which stays blank/`null` and
160+
remains open for a future pass). This distinction matters enough to say
161+
twice: **leaving a CSV cell blank ≠ typing "Skip" into it.**
162+
163+
### Links and corpus need no new data model — and no new capabilities for v0
164+
165+
`person-enrichment` already writes `personal_links` / `personal_corpus`
166+
onto `persons`, and `org_links` / `org_corpus` onto `organizations` (via
167+
`appendOrgLink`/`appendOrgCorpus`/`appendPersonalLink`/`appendPersonalCorpus`
168+
in its `App.svelte`). Reusing that surface as-is means this spec adds
169+
zero new fields and zero new capabilities for the link/corpus half of the
170+
flow.
171+
172+
**Known wart, explicitly not fixed here:** `person-enrichment` writes
173+
directly to SurrealDB from the client, bypassing the NATS-capability
174+
gating (`services/workspace/src/capabilities.ts`) every other write path
175+
in this app goes through. That's pre-existing debt, not introduced by
176+
this spec, and fixing it is a separate concern — noted so it's not
177+
mistaken for an oversight.
178+
179+
### The CSV reimport key is `(person_uuid, org_slug)`, never a raw RecordId
180+
181+
Same lesson this codebase already learned twice (`source_uuid` in
182+
`domains.ts`, `person_uuid` in `person-resolver.ts`): a SurrealDB
183+
`RecordId` doesn't survive a round-trip through anything outside the
184+
server — NATS, JSON, and now (more fragile than either) a CSV file a
185+
human edits by hand in a spreadsheet, which can reformat or mangle text
186+
in ways JSON never would. `person_uuid` and `org_slug` are both already
187+
wire-safe, human-stable identifiers used elsewhere in this app. The
188+
reimport resolver looks each affiliation up fresh by that pair; it never
189+
trusts a RecordId string surviving the trip.
190+
191+
## Where it lives
192+
193+
**Export:** extend `scripts/export-event-attendees-csv.mjs` with the
194+
`relevance`/`relevance_note`/`person_uuid`/`org_slug` columns described
195+
above. Not a new script — this one already pulls affiliation + org + link
196+
+ corpus data per event, per attendee.
197+
198+
**Reimport + resolve:** a new small remote,
199+
`apps/affiliation-rating-resolver` (`affiliationRatingResolver` in
200+
`shell/src/remotes.ts`), port `3012` — the first open gap in the port
201+
sequence. Much smaller than the v0.0.0.1 draft's worklist app: no
202+
match-or-create, no candidate search, just column-mapping (reusing the
203+
established per-record-set mapping pattern) → row iteration → one
204+
capability call per non-blank `relevance` row.
205+
206+
**Links/corpus:** no new app. `person-enrichment`'s `EVENT_SLUG` constant
207+
becomes a picker — the smallest change in this spec.
208+
209+
One new capability, in `services/record-surrealdb-resolver/src/person-resolver.ts`,
210+
registered in `CAPABILITY_TO_SUBJECT` per the existing gating discipline:
211+
212+
| Capability | What it does |
213+
|---|---|
214+
| `affiliation.rate` | Given `(person_uuid, org_slug, relevance, relevance_note)`, look up the live `affiliations` edge fresh and set `relevance`/`relevance_note`/`relevance_rated_by`/`relevance_rated_at`. Throws (surfaced to the operator, not silently dropped) if no matching edge exists. |
215+
216+
## Composes with
217+
218+
- [[Client-Tagging-on-Canonical-Writes]] — the export query and the new
219+
`relevance` fields respect `client_access` the same way every other
220+
canonical write does. See `surrealdb-canonical-layer` (lossless-skills)
221+
for the per-table shape reference.
222+
- [[Sparse-Person-Enrichment-Surface]] — the per-record-set column-mapping
223+
pattern the reimport resolver reuses, and the event-scoped-worklist
224+
discipline the export step follows.
225+
- `context-v/plans/Person-Aware-Canonical-Resolver-Extension.md` — the
226+
person/org/affiliation split this flow's data model builds directly on
227+
top of.
228+
- `context-v/plans/SurrealDB-MCP-Plus-Skill-for-Canonical-Layer-Verification.md`
229+
— the verification pattern that surfaced, on 2026-07-07, that 61 of 65
230+
FreedomFest speakers now have exactly the affiliation edges this flow's
231+
export depends on.
232+
- `scripts/export-event-attendees-csv.mjs`, `scripts/export-event-briefing.mjs`,
233+
and their sibling `export-branded-briefing.mjs` (markdown + brand config
234+
→ branded HTML/PDF) — the export side of this spec extends the first;
235+
the eventual CEO-brief export (out of scope here) has a natural home in
236+
the third once rating data exists to feed it.
237+
238+
## Out of scope for v0.0.0.2
239+
240+
- **The CEO-brief export itself.** Real, wanted, explicitly deferred —
241+
once `relevance`/`relevance_note` exist on every affiliation, that
242+
export is a read-only view over data this spec already produces,
243+
plausibly built on `export-branded-briefing.mjs`'s existing
244+
markdown-plus-brand-config → HTML/PDF path. A much smaller spec to
245+
write later, with real data to design against instead of guesses.
246+
- **Rating a person or org independent of a specific affiliation.**
247+
Someone might eventually want "this org is relevant regardless of which
248+
person," but nothing in the current use case asks for it, and it
249+
reopens the multi-tenant-leak problem the affiliation-scoped design
250+
avoids. Wait for a real case.
251+
- **Bulk / automated relevance scoring.** An LLM could plausibly draft a
252+
first-pass rating from a person's headline + org + corpus content. Not
253+
this spec — manual-first, automate once the manual pattern is proven.
254+
- **Multi-event exports.** One event at a time, same discipline
255+
[[Sparse-Person-Enrichment-Surface]] already settled on.
256+
- **Fixing `person-enrichment`'s direct-SurrealDB write path.** Flagged
257+
above as a known wart. Pre-existing, not created by this spec, not
258+
blocking it.
259+
260+
## Open questions
261+
262+
- Should `relevance`'s five values also gate what shows up in a future
263+
export by default (e.g. blank and `Skip` excluded, `Relevant` and up
264+
included)? Reasonable default, not decided — the export spec's problem.
265+
- `personal_links`/`personal_corpus` naming vs. `org_links`/`org_corpus`
266+
still a live inconsistency, still not fixed here.
267+
- Is a script + CSV-upload round-trip the permanent shape for the rating
268+
pass, or does it earn a proper in-app export/download button once the
269+
pattern proves out? Leaning toward "stays a script" per this app's
270+
manual-first-then-automate discipline, but not decided.
271+
272+
## See also
273+
274+
- [[Sparse-Person-Enrichment-Surface]] — column-mapping and event-scoping
275+
patterns reused here
276+
- [[Client-Tagging-on-Canonical-Writes]] — multi-tenant discipline this
277+
spec's data-model decisions are built around
278+
- `context-v/plans/Person-Aware-Canonical-Resolver-Extension.md` — the
279+
person/org/affiliation schema this flow is additive on top of
280+
- `scripts/export-event-attendees-csv.mjs` — the export this spec extends

0 commit comments

Comments
 (0)