← Context/ issues

Personal-link observations need named query lenses — without them, an accumulating fact log goes uninspected

`has_personal_link` observations carry rich qualifiers and no queries. One thought leader can contribute 50+ before anyone can look.

Context

The Sparse Person Enrichment Surface v0.0.0.1 adds a PersonalLinks pulse-dimension that writes one observation per captured URL with predicate has_personal_link and qualifiers carrying kind, title, url_domain, and (when matched) org_id. Schema:

SQL
CREATE observations SET
  subject     = persons:<X>,
  predicate   = "has_personal_link",
  object      = "https://philanthropyroundtable.org/team/stephen-allison/",
  observed_at = time::now(),
  source      = "person-enrichment",
  qualifiers  = {
    kind:       "team_page",
    title:      "Stephen Allison · The Philanthropy Roundtable",
    url_domain: "philanthropyroundtable.org",
    org_id:     organizations:<phil-roundtable>
  },
  client      = "reach-edu";

That shape is the canonical fact log. The querying lenses aren't built.

The queries we anticipate

Captured here so they're discoverable when someone hits the friction.

Per-person presence rollup

"Show me everywhere we've found Stephen on the public web."

SQL
SELECT object AS url, qualifiers.kind AS kind, qualifiers.title AS title, observed_at
  FROM observations
  WHERE subject = persons:<X> AND predicate = "has_personal_link"
  ORDER BY observed_at DESC;

Useful for: prep before a meeting, generating a credibility card, seeding a briefing doc.

Per-org corpus rollup (lens onto Funder-Content-Corpus)

"Show me all the blog posts, publications, and press releases on philanthropyroundtable.org we've captured, regardless of which person we captured them through."

SQL
SELECT object AS url, subject AS author, qualifiers.kind, qualifiers.title, observed_at
  FROM observations
  WHERE predicate = "has_personal_link"
    AND qualifiers.url_domain = "philanthropyroundtable.org"
    AND qualifiers.kind IN ["blog_post", "publication", "press_release", "research_report"]
  ORDER BY observed_at DESC;

Useful for: the Funder-Content-Corpus lens — the same data we'd otherwise have to crawl the funder's site for. The PersonalLinks dimension is the operator-curated path into corpus; the cron crawler is the automated path. They write the same predicate; this query reads both.

Kind-filtered slice across the whole canonical layer

"Show me everyone we know who's been on a podcast (any podcast)."

SQL
SELECT subject AS person, object AS url, qualifiers.title, observed_at
  FROM observations
  WHERE predicate = "has_personal_link"
    AND qualifiers.kind = "podcast"
    AND client_access CONTAINS $workspace_slug
  ORDER BY observed_at DESC;

(The client_access filter applies via the subject person — handled by materialization or join.)

Useful for: "who'd be a good guest for our event," "who has thought leadership on X."

Team-page-only lens (the "first instance" relationship)

"For each known org, which of our people appears on their team page?"

SQL
SELECT subject AS person, qualifiers.org_id AS org, object AS url, observed_at
  FROM observations
  WHERE predicate = "has_personal_link"
    AND qualifiers.kind = "team_page"
    AND qualifiers.org_id IS NOT NONE
    AND client_access CONTAINS $workspace_slug;

Useful for: the operator's "do we actually know an insider at this org" question — distinct from a vendor relationship or an alumni connection.

What we should index for these to stay fast

At ≤30K entities and the redundancy ethos, indexes are cheap. Adding these when the friction arrives:

SQL
DEFINE INDEX personal_link_kind   ON observations FIELDS predicate, qualifiers.kind;
DEFINE INDEX personal_link_domain ON observations FIELDS predicate, qualifiers.url_domain;
DEFINE INDEX personal_link_org    ON observations FIELDS predicate, qualifiers.org_id;

Not adding them now — premature indexing is a real cost in clarity, and the lenses aren't built yet. Add when one is slow.

Resolution path

This isn't a bug to fix — it's a flagged future-state. Resolution = when an operator workflow hits one of the listed lenses, we either:

  1. Ship the query as a SurrealQL function (DEFINE FUNCTION fn::presence_for_person($id)) — fast, reusable, cacheable.

  2. Ship the query inside the pulse-surface's read path (e.g. show existing personal-links above the input form so the operator sees what's already captured for this person).

  3. Ship a new lens surface that takes a query intent and renders the results.

Default to (1) + (2). (3) is a real undertaking and only justifies itself when ≥3 lenses are in active use.

See also