Skip to content

Commit d9b1dd1

Browse files
mpstatonclaude
andcommitted
feat(org-relations, search-results): team-crawl synopsis persists — organization.add_observation lands as person.add_observation's twin
The team card's synopsis (didi's account of who the selection policy excluded: "~10+ others on the page excluded by policy: Robin Gilbert (VP People & Culture), …") died with the queue card. It now persists as a search_synopsis observation on the ORGANIZATION, written once when the operator first accepts from the card — the crawl itself still never writes, and a failed synopsis write soft-fails and retries on the next accept, never blocking the person. The enabling verb is generic: organization.add_observation (org_slug, predicate, value, source, client) — the org-side twin of person.add_observation, available to didi and future surfaces too. Sibling of #59 (agent_search_rationale on the edge): between them, both layers of the crawl's judgment — per-candidate and per-crawl — survive the queue. Verified: tsc clean both services, svelte-check 0/0, live NATS write + read-back on a throwaway org, zero residue. Closes #60. Files changed: - services/record-surrealdb-resolver/src/org-relations.ts - services/workspace/src/capabilities.ts - apps/search-results/src/SearchCard.svelte - apps/search-results/src/TeamAccept.svelte - apps/search-results/src/lib/search-client.ts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RW28dw3kQAKXr2ZNefCukE
1 parent 34a3845 commit d9b1dd1

5 files changed

Lines changed: 82 additions & 0 deletions

File tree

apps/search-results/src/SearchCard.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
<TeamAccept
170170
people={results.people ?? []}
171171
source_urls={results.source_urls ?? []}
172+
filtered_note={results.filtered_note ?? ''}
172173
org_slug={card.entity.org_slug}
173174
orgName={orgLabel}
174175
{client}

apps/search-results/src/TeamAccept.svelte

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
// broadcast refreshes the org card's People list. Skip discards a row.
1010
1111
import {
12+
addOrgObservation,
1213
addPersonLink,
1314
affiliatePerson,
1415
applyPerson,
@@ -19,13 +20,18 @@
1920
let {
2021
people,
2122
source_urls,
23+
filtered_note = '',
2224
org_slug,
2325
orgName,
2426
client,
2527
onremaining,
2628
}: {
2729
people: CrawledPerson[];
2830
source_urls: string[];
31+
// didi's crawl synopsis (who the policy excluded and why) — persisted
32+
// as a search_synopsis observation on the ORG the first time the
33+
// operator accepts from this card (gh #60).
34+
filtered_note?: string;
2935
org_slug: string;
3036
orgName: string;
3137
client: string;
@@ -55,6 +61,26 @@
5561
return row.person.bio_url ?? source_urls[0] ?? 'didi-crawl';
5662
}
5763
64+
// Write-once: the first successful accept from this card also persists
65+
// didi's synopsis on the org. Soft-fail — the synopsis is context, never
66+
// a reason to fail the accept.
67+
let synopsisWritten = $state(false);
68+
async function persistSynopsis() {
69+
if (synopsisWritten || !filtered_note) return;
70+
synopsisWritten = true;
71+
try {
72+
await addOrgObservation({
73+
org_slug,
74+
predicate: 'search_synopsis',
75+
value: filtered_note,
76+
source: source_urls[0] ?? 'didi-crawl',
77+
client,
78+
});
79+
} catch {
80+
synopsisWritten = false; // retry on the next accept
81+
}
82+
}
83+
5884
async function accept(row: Row) {
5985
row.error = null;
6086
row.phase = 'writing';
@@ -115,6 +141,7 @@
115141
}
116142
}
117143
row.consumed = true;
144+
void persistSynopsis();
118145
window.dispatchEvent(
119146
new CustomEvent('augment-it:entity-updated', { detail: { org_slug } }),
120147
);

apps/search-results/src/lib/search-client.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,22 @@ export async function affiliatePerson(args: {
138138
if (!r.ok) throw new Error(r.error || 'person.affiliate failed');
139139
}
140140

141+
// Free-form org observation — first consumer: the team-crawl
142+
// search_synopsis, written once per card on first accept (gh #60).
143+
export async function addOrgObservation(args: {
144+
org_slug: string;
145+
predicate: string;
146+
value: string;
147+
source?: string;
148+
client: string;
149+
}): Promise<void> {
150+
const r = (await workspace.invoke('organization.add_observation', args)) as {
151+
ok: boolean;
152+
error?: string;
153+
};
154+
if (!r.ok) throw new Error(r.error || 'organization.add_observation failed');
155+
}
156+
141157
export async function addPersonLink(args: {
142158
person_uuid: string;
143159
url: string;

services/record-surrealdb-resolver/src/org-relations.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,40 @@ export async function removeOrgTag(
361361
return { ok: true, removed: ids.length > 0 };
362362
}
363363

364+
// --- organization.add_observation ------------------------------------------
365+
// person.add_observation's twin (gh #60): a free-form observation with an
366+
// ORG as subject. First consumer: the team-crawl search_synopsis (didi's
367+
// account of who the policy excluded), written once when the operator
368+
// first accepts from a card — the crawl itself still never writes.
369+
370+
export type OrgAddObservationInput = {
371+
org_slug: string;
372+
predicate: string;
373+
value: string;
374+
client: string;
375+
source?: string;
376+
};
377+
378+
export async function addOrgObservation(
379+
db: Surreal,
380+
input: OrgAddObservationInput,
381+
): Promise<{ ok: true }> {
382+
const org = await resolveOrgId(db, input.org_slug, input.client);
383+
const predicate = input.predicate.trim();
384+
const value = input.value.trim();
385+
if (!predicate || !value) {
386+
throw new Error('organization.add_observation requires both predicate and value');
387+
}
388+
await observe(db, {
389+
subject: org,
390+
predicate,
391+
object: value,
392+
source: input.source || 'org-workbench',
393+
client: input.client,
394+
});
395+
return { ok: true };
396+
}
397+
364398
// Read helper for getOrgDetail's tags extension — takes the already-resolved
365399
// org RecordId so detail doesn't resolve the slug twice.
366400
export async function listOrgTagsById(
@@ -408,4 +442,5 @@ export function registerOrgRelationHandlers(nc: NatsConnection): void {
408442
handle<OrgRelationUpdateInput>('organization.relation.update.requested', updateOrgRelation);
409443
handle<OrgTagInput>('organization.tag.add.requested', addOrgTag);
410444
handle<OrgTagInput>('organization.tag.remove.requested', removeOrgTag);
445+
handle<OrgAddObservationInput>('organization.add_observation.requested', addOrgObservation);
411446
}

services/workspace/src/capabilities.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@ const CAPABILITY_TO_SUBJECT: Record<string, string> = {
228228
'organization.tag.remove': 'organization.tag.remove.requested',
229229
// Distinct corpus kinds for the kind-input datalist (gh #57).
230230
'organization.corpus.kinds': 'organization.corpus.kinds.requested',
231+
// Free-form org observation — person.add_observation's twin (gh #60).
232+
'organization.add_observation': 'organization.add_observation.requested',
231233

232234
// Domain catalog — the canonical typed-grouping graph behind apps/strategy-curator
233235
// (which is the type='strategy' view). Served by record-surrealdb-resolver
@@ -356,6 +358,7 @@ const CAPABILITY_TIMEOUTS_MS: Record<string, number> = {
356358
'organization.tag.add': 30_000,
357359
'organization.tag.remove': 30_000,
358360
'organization.corpus.kinds': 30_000,
361+
'organization.add_observation': 30_000,
359362
'client.brief.get': 30_000,
360363
'client.brief.set': 30_000,
361364
// A crawl is one model turn with multiple server-side web searches (plus

0 commit comments

Comments
 (0)