← Corpus / augment-it / plan
Workbench usability sweep — corpus visibility, stream editing, and promote-to-affiliation (issues #20 · #26 · #25)
Today's scope from the first real workbench session: make the person card's three invisible corpus items visible, let the operator fix a stream's kind and give it its real name, and turn a bio-page link row into a three-click affiliation.
- Path
- plans/Workbench-Usability-Sweep-Corpus-Visibility-Stream-Editing-Affiliation-Promotion.md
- Authors
- Michael Staton
- Augmented with
- Claude Code on Claude Fable 5
- Tags
- Plan · Augment-It · Org-Workbench · Corpus · Pulse-Streams · Affiliations · Usability
Workbench usability sweep — #20 · #26 · #25
Issue references
Executes three of the 2026-07-24 workbench-session issues, in ascending complexity:
- A. [[../issues/Corpus-Items-Not-Visible-On-Person-Cards-Coverage-Hard-To-Assess]] — gh #20
- B. [[../issues/Pulse-Streams-Need-Editable-Kind-And-User-Facing-Names]] — gh #26
- C. [[../issues/Person-Bio-Pages-Are-Affiliation-Signals-Not-Just-Identity-Links]] — gh #25
Branch: rebuild/turbo-rsbuild. All UI lands in apps/org-workbench; service changes are confined to services/record-surrealdb-resolver + the capability map in services/workspace. Close each gh issue as its slice ships (the new create-task/close-task habit), and move each issue doc’s status on the same commit.
Decisions taken in this plan (resolving the issues’ open questions)
- #20 goes eager, not lazy. Extend
listOrgAffiliationsto carrypersonal_corpus[]alongside the existing count. Rejected lazy (affiliation.detailon expand):PersonCardmounts with the already-loaded affiliation row and no detail wrapper exists inorg-client.ts— lazy is not zero-work on this frontend, and eager makes corpus symmetric withpersonal_links, which are already eager. Payload is single-digit entries per person; the 8MB NATS ceiling is nowhere near. - #20 v1 renders URL+kind, no titles.
content_itemshas no title field at all today (findOrCreateContentwrites only url/domain/kind/counters —resolver.ts:364-369); “titles live in the ledger” was optimistic. Title hydration = a rider for the content-ingest path, not this sweep. - #20’s coverage roster (layer 2) is out of scope today. The per-card fix ships now; the “which entities have NO corpus?” roster folds into the component-library sweep ([[../issues/No-Component-Library-UI-Improvised-Not-Component-Based]] / gh #22) where thin-row rosters get built once, properly.
- #26’s update verb is a match-by-URL patch, not remove+re-add. Precedent:
resolver.update_org(resolver.ts:709-767) already does sparseSETpatches withlast_touched_*stamping. URL is the de-facto stream key everywhere (dedupe, scan). Safe because streamkindhas zero routing effect today —stream-scan.tsdeclaresstream_kindand ignores it; every scan routes torunOfficialBlogPack. - #26 also adds
topic_hubtoinferStreamKind— one regex line fixes the/topics/…misclassification at the source; the editable kind remains the correction path for everything else. - #25 v1 lives on the link row, not in didi-chat. The
AdditiveListentryactionslot exists and is unused on person links — that’s the mounting point. A chat verb (CURATOR_CHAT_VERBSblock +CHAT_CAPABILITY_NAMESallow-list,chat.ts:87-129) is a named follow-up, not this sweep. - #25 creates orgs thin — but with a domain.
resolveOrgRow’s create branch seeds nodomains[](resolver.ts:606-614), so an org created from a bio link would be invisible to future D4 domain matching. ExtendPersonAffiliateInputwith optionalorg_domain, seeded intodomains[]on the create branch only. No full org-resolution gate for v1. - #25’s observation is
affiliated_withwith the bio URL assource.PersonAffiliateInput.sourcealready exists and flows into the auto-observation (person-resolver.ts:562-568) — pass the link URL; no separatehas_bio_atpredicate for v1 (free-text predicates viaperson.add_observationremain available if the distinction earns its keep later).
Steps
A — Corpus items visible on person cards (gh #20)
- Query —
listOrgAffiliations(services/record-surrealdb-resolver/src/person-resolver.ts:857-864): addin.personal_corpus ?? [] AS personal_corpusbeside the existingarray::len(...) AS personal_corpus_count(count stays —PeopleReveal’s collapsed rows use it). - Types —
AffiliatedPersongainspersonal_corpus: ShapedLink[]in both mirrors:person-resolver.ts:835-843andapps/org-workbench/src/lib/types.ts:61. - Render —
PersonCard.svelte:82-108: replace the count-only corpus block with anAdditiveList(same reuse as links at:74-80): title “Corpus items”, entriesperson.personal_corpus,onadd→ existingperson.corpus.add,onsearch→ existing corpus-target envelope. Row shape comes free: kind badge · host · date (AdditiveList.svelte:106-118). Delete the bespoke count/➕/🔍 markup; also remove the now-false scope comment atPersonCard.svelte:2-6. - Verify — svelte-check + build org-workbench + shell build regression; then live: Lumina → People → Jamie Merisotis shows the three entries that today render as “Corpus items 3” and nothing.
B — Stream name + editable kind (gh #26)
- Shape —
ShapedStream(resolver.ts:32-38) andshapeStream(:146-157) gain optionalname;OrgStreamAddInput(:838) gainsname?, passed throughaddOrgStream(:841-854). SurrealDB is schemaless — no migration. - Classifier —
inferStreamKind(resolver.ts:117-133): addtopic_hubfor paths matchingtopics|/tag/|/category/, ahead of theupdates_indexfallback. - Update verb — new
updateOrgStream(db, {org_slug, url, kind?, name?, client})besideaddOrgStream: loadmedia_streams, patch the entry whose trimmedurlmatches,UPDATE ... SET media_streams = $patched+ theclient_accessunion /last_touched_*stampsupdateOrgalready models (resolver.ts:739-741). Throw if no entry matches. New subscriptionorganization.streams.update.requestedinhandlers.ts(beside:182-198); capability + 30s timeout inservices/workspace/src/capabilities.ts(beside:184/:291); wrapper inapps/org-workbench/src/lib/org-client.ts(besideaddOrgStream,:61-69). - UI —
AdditiveList.svelte: add form (:81-101) gains a name input, threaded throughonadd; entry rows rendername(falling back tohost(url)) at:110; kind badge (:109) and name become click-to-edit by adaptingapps/records-surface/src/components/EditableField.svelte, committing via a new optionalonedit(url, patch)prop so the component stays generic —OrgCard.sveltepasses it only for the streams list (→updateOrgStream→ existingmakeAdd-style refetch +augment-it:entity-updated). The “Additive only” header comment (AdditiveList.svelte:1-7) gets amended: additive entries, patchable fields. - Verify — builds as in step 4; live: rename Lumina’s
todays-credentialsstream to “Today’s Credentials”, correct its kind, confirm the row survives a refetch and a scan still routes (kind is not consumed bystream-scan.ts— confirm no regression, not new behavior).
C — Promote a bio link to an affiliation (gh #25)
- Verb extension —
PersonAffiliateInput(person-resolver.ts:507-519) gainsorg_domain?;resolveOrgRow’s create branch (resolver.ts:606-614) seedsdomains: [{domain: $org_domain}]when provided. TheaffiliatePersonwrapper (org-client.ts:118-134) un-hardwiresorg_action: 'match'and exposes the full input. AddAffiliationInline.svelte— new org-workbench component:AddPersonInline’s form → gate → writing state machine (AddPersonInline.svelte:28-83) inverted — person fixed (from the card), org being resolved. Seeded from the link row: org-name field pre-filled empty, domain pre-filled from the link’s hostname; candidates viaresolver.search(D4 domain clause already matchesdomains[*].domain—resolver.ts:327), queried with the domain. Gate ALWAYS: pick a candidate (org_action:'match', its slug) or “Create new org + affiliate” (org_action:'create', typed name + seededorg_domain). Optional role input →role.source= the bio URL. On success: dispatchaugment-it:entity-updated {person_uuid}(the existing refetch path,PeopleReveal.svelte:50-55).- Mounting —
PersonCard.svelte:74-80: passentryaction: {label: '→ affiliation', fn: openPromote(entry)}on the personal-linksAdditiveList(slot exists unused —AdditiveList.svelte:25-27, 111-115); the action opensAddAffiliationInlineunder the list, bound to that entry. - Riders, named not built — (a) port the bio-relevant kinds (
author_bio,team_page,publication) fromapps/person-enrichment/src/pulse-dimensions/LinkList.svelte:41-51into the serverinferLinkKind(resolver.ts:91-114) so future bio links stop badgingother; (b) same promote affordance on search-and-add result rows (ResultRow.svelte+verbFor,search-client.ts:65-82); (c) didi-chat promotion verb per Decision 6. Each is a small follow-up once v1 proves the flow. - Verify — builds as in step 4. The write path is NOT exercised headlessly —
person.affiliatecreates real canonical orgs/edges/observations, and the additive-writes discipline reserves browser-driven writes for the designated safe target (Aspen Institute card). Mechanical drive: browser-drive the gate open/candidates/cancel path read-only (Playwright MCP is wired in.mcp.jsonbut loads next session — if unavailable, this rung falls to the walk-through). Operator walk-through: Jamie Merisotis → bipartisanpolicy.org row → ”→ affiliation” → gate shows candidates (or none) → create “Bipartisan Policy Center” with domain → affiliation + observation appear; re-open the gate on the same row → the new org now domain-matches.
Tail
- Changelog entry per [[changelog-conventions]]; commit per [[git-conventions]] (one commit per slice:
fix(org-workbench, corpus): …,new(streams, resolver): …,new(org-workbench, affiliations): …); push;gh issue close 20/26/25 --reason completedas each slice lands; flip the three issue docs toShippedwithdate_first_publishedon the same commits.