Skip to content

Commit b6ec4da

Browse files
mpstatonclaude
andcommitted
attempt(augment-from-db, org-workbench, step2): the sixth flow gets its front door — org search, org card, and a ➕ on every list
apps/org-workbench (:3014) is the first of the flow's two new remotes: pick "Augment from DB" in the Flows popdown, autocomplete to a canonical org over the alias-aware resolver.search (300ms debounce, stale-response guard), and work one card that views AND edits in place — identity/social links, pulse streams, corpus items, each an AdditiveList with per-list localized errors and an "added ✓" pulse. Credential-free per spec D1; client derived from workspace.active with workspace-changed handling (a client switch drops the card); last-worked org restored on remount. One spec gap closed: media_streams[] had no single-entry verb (streams only ever arrived via resolver.apply's batch path), so the org card's pulse-streams ➕ had nothing to call. organization.streams.add now mirrors addOrgLink — shapeStream reuse, kind auto-inferred, party 'first_party' — with handler + capability map + timeout. Shell registration is exactly the shape the front-door refactor promised: one FLOWS entry, one rotation array, one REMOTES entry, one federation map line. Proof: svelte-check 0/0; org-workbench + shell build (remoteEntry.js, HTTP 200 smoke on :3014); services typecheck; streams.add live against The Aspen Institute (0→1, blog_index/first_party inferred, visible on detail re-read); Phase 1 proof re-run 7/7 green. Plan: context-v/plans/Augment-From-DB-Phase-2-Org-Workbench-Remote.md (Shipped). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014zrFkWSVgTkoQyiobdBjrd
1 parent 8d141ca commit b6ec4da

22 files changed

Lines changed: 921 additions & 0 deletions

apps/org-workbench/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@augment-it/org-workbench",
3+
"version": "0.0.1",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "rsbuild dev",
8+
"build": "rsbuild build",
9+
"preview": "rsbuild preview",
10+
"check": "svelte-check --tsconfig ./tsconfig.json"
11+
},
12+
"dependencies": {
13+
"@augment-it/theme": "workspace:*",
14+
"@augment-it/workspace": "workspace:*",
15+
"svelte": "^5.56.4"
16+
},
17+
"devDependencies": {
18+
"@module-federation/enhanced": "^2.6.0",
19+
"@module-federation/rsbuild-plugin": "^2.6.0",
20+
"@rsbuild/core": "^2.1.2",
21+
"@rsbuild/plugin-svelte": "^2.0.0",
22+
"svelte-check": "^4.7.1",
23+
"typescript": "^6.0.3"
24+
}
25+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { defineConfig } from '@rsbuild/core';
2+
import { pluginSvelte } from '@rsbuild/plugin-svelte';
3+
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
4+
5+
// Federated remote — org-workbench. The first surface of the "Augment from
6+
// DB" flow: start from a canonical SurrealDB organization (not a CSV row),
7+
// smart-search to it, and work its card — identity/social links, pulse
8+
// streams, corpus items — with an additive ➕ on every list. Credential-free
9+
// by design: every read/write rides workspace.invoke → NATS →
10+
// record-surrealdb-resolver (spec decision D1).
11+
// See context-v/specs/Augment-From-DB-Flow.md.
12+
export default defineConfig({
13+
plugins: [
14+
pluginSvelte(),
15+
pluginModuleFederation({
16+
name: 'orgWorkbench',
17+
filename: 'remoteEntry.js',
18+
exposes: {
19+
'./mount': './src/mount.ts',
20+
},
21+
dts: false,
22+
}),
23+
],
24+
source: {
25+
entry: { index: './src/index.ts' },
26+
},
27+
output: {
28+
target: 'web',
29+
overrideBrowserslist: ['last 2 Chrome versions', 'last 2 Firefox versions', 'last 2 Safari versions'],
30+
},
31+
tools: {
32+
swc: {
33+
jsc: { target: 'es2022' },
34+
},
35+
},
36+
html: {
37+
title: 'augment-it · org-workbench',
38+
},
39+
server: {
40+
port: 3014,
41+
cors: { origin: ['http://localhost:3100'] }, // the federation shell
42+
},
43+
dev: {
44+
assetPrefix: 'http://localhost:3014',
45+
},
46+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<script lang="ts">
2+
// Generic additive list — the org card's repeated organ. Renders shaped
3+
// entries (kind badge · host · date) with an inline ➕ form that hands the
4+
// URL (+ optional kind) to a caller-supplied add function. Additive only:
5+
// no edit, no delete — canonical writes are append + dedup server-side.
6+
// Busy/error states are localized to this list; a failed add never
7+
// disturbs the sibling lists.
8+
9+
import type { ShapedLink } from './lib/types';
10+
11+
let {
12+
title,
13+
entries,
14+
kindHint = 'auto-detected from URL',
15+
onadd,
16+
}: {
17+
title: string;
18+
entries: ShapedLink[];
19+
kindHint?: string;
20+
onadd: (url: string, kind?: string) => Promise<void>;
21+
} = $props();
22+
23+
let adding = $state(false);
24+
let open = $state(false);
25+
let url = $state('');
26+
let kind = $state('');
27+
let error = $state<string | null>(null);
28+
let justAdded = $state(false);
29+
30+
async function submit(e: SubmitEvent) {
31+
e.preventDefault();
32+
const trimmed = url.trim();
33+
if (!trimmed) return;
34+
adding = true;
35+
error = null;
36+
try {
37+
await onadd(trimmed, kind.trim() || undefined);
38+
url = '';
39+
kind = '';
40+
open = false;
41+
justAdded = true;
42+
setTimeout(() => (justAdded = false), 2000);
43+
} catch (err) {
44+
error = err instanceof Error ? err.message : String(err);
45+
} finally {
46+
adding = false;
47+
}
48+
}
49+
50+
function host(u: string): string {
51+
try {
52+
return new URL(u).hostname.replace(/^www\./, '');
53+
} catch {
54+
return u;
55+
}
56+
}
57+
</script>
58+
59+
<section class="ow-list">
60+
<header class="ow-list-head">
61+
<h3 class="ow-list-title">{title} <span class="ow-list-count">{entries.length}</span></h3>
62+
<span class="ow-list-actions">
63+
{#if justAdded}<span class="ow-added">added ✓</span>{/if}
64+
<button type="button" class="ow-plus" title="Add to {title}" onclick={() => (open = !open)}>
65+
{open ? '×' : '+'}
66+
</button>
67+
</span>
68+
</header>
69+
70+
{#if open}
71+
<form class="ow-add" onsubmit={submit}>
72+
<input
73+
class="ow-add-url"
74+
type="url"
75+
placeholder="https://…"
76+
bind:value={url}
77+
required
78+
disabled={adding}
79+
/>
80+
<input
81+
class="ow-add-kind"
82+
type="text"
83+
placeholder={kindHint}
84+
bind:value={kind}
85+
disabled={adding}
86+
/>
87+
<button type="submit" class="ow-add-go" disabled={adding}>{adding ? '' : 'Add'}</button>
88+
</form>
89+
{#if error}<div class="ow-error">{error}</div>{/if}
90+
{/if}
91+
92+
{#if entries.length === 0}
93+
<p class="ow-empty">none yet</p>
94+
{:else}
95+
<ul class="ow-entries">
96+
{#each entries as e (e.url + e.added_at)}
97+
<li class="ow-entry">
98+
<span class="ow-kind">{e.kind}</span>
99+
<a class="ow-url" href={e.url} target="_blank" rel="noreferrer">{host(e.url)}</a>
100+
<span class="ow-date">{(e.added_at ?? '').slice(0, 10)}</span>
101+
</li>
102+
{/each}
103+
</ul>
104+
{/if}
105+
</section>

apps/org-workbench/src/App.svelte

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<script lang="ts">
2+
// org-workbench — the first surface of the "Augment from DB" flow. Start
3+
// from a canonical SurrealDB organization: smart-search to it, then work
4+
// its card (identity/social links, pulse streams, corpus items) with an
5+
// additive ➕ on every list. Credential-free (spec D1) — everything rides
6+
// workspace.invoke. Client-derivation + workspace-changed handling copied
7+
// from person-db-resolver. See context-v/specs/Augment-From-DB-Flow.md.
8+
9+
import { onMount } from 'svelte';
10+
import { workspace } from '@augment-it/workspace';
11+
import OrgSearch from './OrgSearch.svelte';
12+
import OrgCard from './OrgCard.svelte';
13+
import { fetchOrgDetail } from './lib/org-client';
14+
import type { OrgDetail, OrgSuggestion } from './lib/types';
15+
16+
const TOKEN_KEY = 'augment-it:session-token';
17+
const WS_URL = 'ws://localhost:3001/ws';
18+
// Restore the last-worked org on remount (HMR, flow switch, tab reopen).
19+
const ACTIVE_ORG_KEY = 'augment-it:org-workbench:active-org';
20+
21+
let status = $state<'connecting' | 'open' | 'closed' | 'error'>('connecting');
22+
let client = $state<string>('reach-edu');
23+
24+
let org = $state<OrgDetail | null>(null);
25+
let loading = $state(false);
26+
let error = $state<string | null>(null);
27+
28+
async function loadOrg(org_slug: string) {
29+
loading = true;
30+
error = null;
31+
try {
32+
org = await fetchOrgDetail(org_slug, client);
33+
if (typeof localStorage !== 'undefined') localStorage.setItem(ACTIVE_ORG_KEY, org_slug);
34+
} catch (err) {
35+
error = err instanceof Error ? err.message : String(err);
36+
org = null;
37+
} finally {
38+
loading = false;
39+
}
40+
}
41+
42+
function onPick(s: OrgSuggestion) {
43+
void loadOrg(s.slug);
44+
}
45+
46+
function refetch() {
47+
if (org) void loadOrg(org.slug);
48+
}
49+
50+
function onEntityUpdated(e: Event) {
51+
const detail = (e as CustomEvent).detail as { org_slug?: string } | undefined;
52+
if (detail?.org_slug && org && detail.org_slug === org.slug) refetch();
53+
}
54+
55+
function onWorkspaceChanged(e: Event) {
56+
const detail = (e as CustomEvent).detail as { client_id?: string } | undefined;
57+
if (detail?.client_id) client = detail.client_id;
58+
else void loadActiveClient();
59+
// A different client sees a different slice of the canonical layer —
60+
// drop the card rather than show rows the new client may not access.
61+
org = null;
62+
}
63+
64+
async function loadActiveClient() {
65+
try {
66+
const r = (await workspace.invoke('workspace.active', {})) as { active_client_id?: string };
67+
if (r?.active_client_id) client = r.active_client_id;
68+
} catch {
69+
/* keep default */
70+
}
71+
}
72+
73+
onMount(() => {
74+
workspace.connect({
75+
url: WS_URL,
76+
getToken: () => localStorage.getItem(TOKEN_KEY),
77+
saveToken: (t) => localStorage.setItem(TOKEN_KEY, t),
78+
onStatus: (s) => (status = s),
79+
});
80+
void (async () => {
81+
await loadActiveClient();
82+
const stored = typeof localStorage !== 'undefined' ? localStorage.getItem(ACTIVE_ORG_KEY) : null;
83+
if (stored) void loadOrg(stored);
84+
})();
85+
window.addEventListener('augment-it:workspace-changed', onWorkspaceChanged);
86+
window.addEventListener('augment-it:entity-updated', onEntityUpdated);
87+
return () => {
88+
window.removeEventListener('augment-it:workspace-changed', onWorkspaceChanged);
89+
window.removeEventListener('augment-it:entity-updated', onEntityUpdated);
90+
};
91+
});
92+
</script>
93+
94+
<div class="ow-app">
95+
<header class="ow-header">
96+
<div class="ow-title-row">
97+
<h1 class="ow-title">Org Workbench</h1>
98+
<span class="ow-source">SurrealDB · Organizations</span>
99+
<span class="ow-client">client: <strong>{client}</strong></span>
100+
<span class="ow-ws status-{status}">{status}</span>
101+
</div>
102+
<OrgSearch {client} onpick={onPick} />
103+
</header>
104+
105+
<main class="ow-body">
106+
{#if loading}
107+
<p class="ow-loading">loading…</p>
108+
{:else if error}
109+
<div class="ow-error">{error}</div>
110+
{:else if org}
111+
<OrgCard {org} {client} onchanged={refetch} />
112+
{:else}
113+
<p class="ow-empty-state">
114+
Search for an organization above to open its card — links, pulse streams, corpus items,
115+
and (soon) its people.
116+
</p>
117+
{/if}
118+
</main>
119+
</div>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<script lang="ts">
2+
// The org card — one screen that views AND edits in place (the
3+
// Augment-From-Affiliations operator ruling: no "two loops, two apps").
4+
// Identity block up top, then the three additive lists. Every successful
5+
// add triggers the parent's refetch so the card always shows DB truth,
6+
// never an optimistic guess.
7+
8+
import AdditiveList from './AdditiveList.svelte';
9+
import { addOrgLink, addOrgStream, addOrgCorpus } from './lib/org-client';
10+
import type { OrgDetail } from './lib/types';
11+
12+
let {
13+
org,
14+
client,
15+
onchanged,
16+
}: {
17+
org: OrgDetail;
18+
client: string;
19+
onchanged: () => void;
20+
} = $props();
21+
22+
function makeAdd(fn: (args: { org_slug: string; url: string; kind?: string; client: string }) => Promise<unknown>) {
23+
return async (url: string, kind?: string) => {
24+
await fn({ org_slug: org.slug, url, kind, client });
25+
onchanged();
26+
window.dispatchEvent(
27+
new CustomEvent('augment-it:entity-updated', { detail: { org_slug: org.slug } }),
28+
);
29+
};
30+
}
31+
</script>
32+
33+
<article class="ow-card">
34+
<header class="ow-card-head">
35+
<h2 class="ow-card-name">{org.complete_name ?? org.conventional_name ?? org.slug}</h2>
36+
<code class="ow-card-slug">{org.slug}</code>
37+
</header>
38+
39+
<dl class="ow-identity">
40+
{#if org.conventional_name && org.conventional_name !== org.complete_name}
41+
<dt>Known as</dt>
42+
<dd>{org.conventional_name}</dd>
43+
{/if}
44+
{#if org.aliases.length > 0}
45+
<dt>Aliases</dt>
46+
<dd>{org.aliases.join(' · ')}</dd>
47+
{/if}
48+
{#if org.domains.length > 0}
49+
<dt>Domains</dt>
50+
<dd>{org.domains.map((d) => d.domain).filter(Boolean).join(' · ')}</dd>
51+
{/if}
52+
</dl>
53+
54+
<div class="ow-lists">
55+
<AdditiveList
56+
title="Identity & social links"
57+
entries={org.org_links}
58+
kindHint="kind (auto: website/linkedin/x/…)"
59+
onadd={makeAdd(addOrgLink)}
60+
/>
61+
<AdditiveList
62+
title="Pulse streams"
63+
entries={org.media_streams}
64+
kindHint="kind (auto: blog_index/rss/newsroom/…)"
65+
onadd={makeAdd(addOrgStream)}
66+
/>
67+
<AdditiveList
68+
title="Corpus items"
69+
entries={org.org_corpus}
70+
kindHint="kind (optional)"
71+
onadd={makeAdd(addOrgCorpus)}
72+
/>
73+
</div>
74+
</article>

0 commit comments

Comments
 (0)