Contact / dossier data model

A shared model for people-dossiers, used by this repo (Incline community directory) and
the querystory-sales-kit (event contacts). Goal: keep the rich prose we already write, but
make the structured facts queryable so we can render views (by relationship, by tag, by
event) without maintaining a second copy of the data by hand.

The problem we’re solving

What we write today is structured markdown: a fixed key-value header, a semi-fixed set of
## sections, and freeform prose underneath. Alongside it we keep a separate hand-written
JSON index (people.json, attendees.json). Two copies of the same facts = guaranteed
drift. Sales-kit already shows the failure mode: profiles carry “⚠️ the event JSON is stale”
notes because the structured side and the prose side diverged.

So the design question isn’t “JSON or markdown” — it’s which one is the source of truth,
and how does the other get generated.

Decision: frontmatter is the source of truth; the index is generated

One file per person. The structured facts move up into YAML frontmatter. The prose stays
in the markdown body. A build step emits the JSON index from the frontmatter — never
hand-edited. This is the standard static-site pattern and it dissolves the drift problem:
there is exactly one place each fact lives.

---
name: Erik Fair
slug: erik-fair
groups: [ski-way-villas]        # a person can be in several
relationship: neighbor
unit: "316 Ski Way"
confidence: confirmed
captured: 2026-07-27
links:
  linkedin: https://www.linkedin.com/in/erikfair/
tags: [netbsd, dns, ntp, infosec, hoa-board]
hooks:                          # short, structured — the "why we care" bullets
  - "ran NetBSD in the 90s = the OS he builds"
  - "Apple Hostmaster ↔ Shapor ran DNS at Google"
roles: ["HOA President (Diamond Ridge OA)"]
---

## Background
Long-form prose lives here, exactly as we write it now…

## Networking notes
…

Why frontmatter and not “parse the **Captured:** header block”: parsing prose is brittle
(every author formats it slightly differently, and it breaks the moment someone edits a
line). Frontmatter is a real typed key-value store the generator reads for free. The header
block we write today simply becomes the frontmatter — same facts, moved above the ---.

What’s structured vs. prose (the boundary)

Rule of thumb: if a future “show me all X” view would need it, it’s frontmatter. If it’s a
paragraph you’d read to a spouse over coffee, it’s body.

Views come from querying frontmatter

Once facts are in frontmatter, views are trivial and need no extra data:

Two rendering surfaces, same data:

  1. Server-rendered (Hugo taxonomies): groups and tags as taxonomies give section
    pages for free, no JS. Good default for the directory landing.
  2. JS viewer: consumes the generated index.json for instant client-side filter/search
    across everything. Same static-hosting story — it’s just a JSON file + a script in the
    bucket. Add when we want live filtering; not required for v1.

Generating the index (no second source of truth)

Shared schema, per-repo extensions

Both repos use the same core person fields above. Each adds what it needs as optional fields,
so one viewer/renderer can serve both:

Unknown fields are ignored by the renderer, so extending one repo never breaks the other.

Migration path (incremental, non-breaking)

  1. Add frontmatter to each *.md (lift the header block up); leave the body untouched.
  2. Add the generator (Hugo output format here; script in sales-kit) → produces index.json.
  3. Point the directory view at the generated index; delete the hand-maintained people.json.
  4. Optional later: the JS viewer for client-side filtering.

Do this one file at a time — a profile without frontmatter still renders fine as prose, so
there’s no flag day.

Why not the alternatives