Skip to content

Commit 251dda4

Browse files
mpstatonclaude
andcommitted
attempt(augment-from-db, stream-scan, step5): scan a pulse stream, badge the known, one-click the new — and the spec goes Shipped
Spec step 8 lands as a mode of search-and-add (decision D3), and with it all five phases of Augment-From-DB-Flow.md are implemented — one day, one signed-off spec, five plans, five proofs. The cheap trick: runOfficialBlogPack already had the exact seam. curated_index_urls skips discovery entirely when the operator has curated the index pages — and a stream scan IS "the index is curated: it's the stream URL." organization.stream.scan is a thin wrapper: fire the blog machinery at the stream, dedup via the new content.urls.check on record-surrealdb-resolver (social-search has no DB access by design; cross-service NATS read per the domains.ts precedent), reply items with already_in_corpus flags. On the card: AdditiveList grew an optional per-entry action; the Pulse-streams list wires it as "scan," dispatching a stream-carrying envelope through the same localStorage+event+navigate contract as every 🔍. In search-and-add: a stream envelope flips the surface into scan mode — TermBar/palette give way to the stream URL + Re-scan, known rows badge "in corpus" with the ➕ parked, fresh rows one-click into org_corpus. Social walls ride the same call flagged experimental (spec non-goal); blog/RSS/newsroom is the proven path. Proof, live against the stream Phase 2 seeded: scan of aspeninstitute.org/blog/ → 8 dated July-2026 posts → corpus.add one → re-scan shows already_known:1 with the added item's flag flipped true. svelte-check 0/0 both remotes (one prop-destructure miss caught + fixed), all builds, three service typechecks, Phase 1 proof 7/7. Spec: context-v/specs/Augment-From-DB-Flow.md → Shipped (v0.1.0.0); operator browser walk-throughs + parked open questions noted in its post_ship_note. Plan: context-v/plans/Augment-From-DB-Phase-5-Stream-Scan-Mode.md (Shipped). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014zrFkWSVgTkoQyiobdBjrd
1 parent 334d683 commit 251dda4

17 files changed

Lines changed: 382 additions & 11 deletions

File tree

apps/org-workbench/src/AdditiveList.svelte

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414
kindHint = 'auto-detected from URL',
1515
onadd,
1616
onsearch,
17+
entryaction,
1718
}: {
1819
title: string;
1920
entries: ShapedLink[];
2021
kindHint?: string;
2122
onadd: (url: string, kind?: string) => Promise<void>;
2223
// Optional 🔍 — launches search-and-add pre-scoped to this list (Phase 3).
2324
onsearch?: () => void;
25+
// Optional per-entry action (Phase 5 — "scan" on pulse streams).
26+
entryaction?: { label: string; fn: (entry: ShapedLink) => void };
2427
} = $props();
2528
2629
let adding = $state(false);
@@ -105,6 +108,11 @@
105108
<li class="ow-entry">
106109
<span class="ow-kind">{e.kind}</span>
107110
<a class="ow-url" href={e.url} target="_blank" rel="noreferrer">{host(e.url)}</a>
111+
{#if entryaction}
112+
<button type="button" class="ow-entry-action" onclick={() => entryaction.fn(e)}>
113+
{entryaction.label}
114+
</button>
115+
{/if}
108116
<span class="ow-date">{(e.added_at ?? '').slice(0, 10)}</span>
109117
</li>
110118
{/each}

apps/org-workbench/src/OrgCard.svelte

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@
8080
kindHint="kind (auto: blog_index/rss/newsroom/…)"
8181
onadd={makeAdd(addOrgStream)}
8282
onsearch={makeSearch('streams', (n) => `"${n}" blog`)}
83+
entryaction={{
84+
label: 'scan',
85+
fn: (stream) =>
86+
requestSearch({
87+
entity: { type: 'organization', org_slug: org.slug, display_name: displayName },
88+
target: 'corpus',
89+
seed_term: '',
90+
stream: { url: stream.url, kind: stream.kind },
91+
}),
92+
}}
8393
/>
8494
<AdditiveList
8595
title="Corpus items"

apps/org-workbench/src/app.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
.ow-url { color: var(--color-link, #8ab4f8); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
6464
.ow-url:hover { text-decoration: underline; }
6565
.ow-date { margin-left: auto; flex-shrink: 0; color: var(--color-text-muted, #9aa0aa); font-size: 0.72rem; font-variant-numeric: tabular-nums; }
66+
.ow-entry-action { flex-shrink: 0; font-size: 0.7rem; padding: 1px 8px; border-radius: 999px; border: 1px solid var(--color-border, #2a2c33); background: var(--color-bg, #14151a); color: var(--color-text-muted, #cfd3da); font-family: inherit; cursor: pointer; }
67+
.ow-entry-action:hover { background: var(--color-border, #2a2c33); color: var(--color-text, #e7e7e7); }
6668

6769
.ow-empty { color: var(--color-text-muted, #9aa0aa); font-size: 0.8rem; margin: 0.4rem 0 0; }
6870
.ow-error { color: var(--color-error-text, #f3a3a3); font-size: 0.8rem; margin-top: 0.4rem; }

apps/org-workbench/src/lib/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,8 @@ export type SearchRequestDetail = {
7171
target: 'links' | 'corpus' | 'streams';
7272
seed_term: string;
7373
intent?: string;
74+
// Phase 5 — when present, search-and-add enters scan mode: the stream URL
75+
// is scanned via organization.stream.scan instead of a term search, and
76+
// ➕ lands items in org_corpus (target is 'corpus' for scan envelopes).
77+
stream?: { url: string; kind?: string };
7478
};

apps/search-and-add/src/App.svelte

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import ProviderPalette from './ProviderPalette.svelte';
1616
import ResultsList from './ResultsList.svelte';
1717
import { searchContext } from './lib/search-context.svelte';
18-
import { fireSearch, fetchConnectors, addResult, verbFor } from './lib/search-client';
18+
import { fireSearch, fetchConnectors, addResult, verbFor, scanStream } from './lib/search-client';
1919
import type { ConnectorInfo, ConnectorResult } from './lib/types';
2020
2121
const TOKEN_KEY = 'augment-it:session-token';
@@ -36,6 +36,9 @@
3636
let firedArrival = -1;
3737
3838
const req = $derived(searchContext.request);
39+
// Phase 5 — scan mode: the envelope carries a stream. No term, no palette;
40+
// the stream URL is the query and organization.stream.scan is the fire.
41+
const scanMode = $derived(Boolean(req?.stream?.url && req?.entity.type === 'organization'));
3942
const entityLabel = $derived(
4043
req
4144
? req.entity.display_name ??
@@ -52,6 +55,7 @@
5255
});
5356
5457
async function fire() {
58+
if (scanMode) return void scan();
5559
if (!term.trim()) return;
5660
firing = true;
5761
fireError = null;
@@ -72,6 +76,28 @@
7276
}
7377
}
7478
79+
async function scan() {
80+
if (!req?.stream?.url || req.entity.type !== 'organization') return;
81+
firing = true;
82+
fireError = null;
83+
try {
84+
const r = await scanStream({
85+
org_slug: req.entity.org_slug,
86+
stream_url: req.stream.url,
87+
stream_kind: req.stream.kind,
88+
client,
89+
});
90+
results = r.results;
91+
firedVia = `stream scan (${r.already_known} already in corpus)`;
92+
} catch (err) {
93+
fireError = err instanceof Error ? err.message : String(err);
94+
results = [];
95+
firedVia = null;
96+
} finally {
97+
firing = false;
98+
}
99+
}
100+
75101
// A fresh envelope (mount-time localStorage read counts, via arrival 0 vs
76102
// firedArrival -1) seeds the term and auto-fires exactly once. Later
77103
// operator edits + re-fires never re-trigger this.
@@ -82,7 +108,7 @@
82108
term = req.seed_term;
83109
results = [];
84110
firedVia = null;
85-
void fire();
111+
void (scanMode ? scan() : fire());
86112
}
87113
}
88114
});
@@ -139,8 +165,19 @@
139165
{/if}
140166
<span class="saa-ws status-{status}">{status}</span>
141167
</div>
142-
<TermBar bind:term {firing} onfire={fire} />
143-
<ProviderPalette {connectors} bind:selected={selectedProvider} />
168+
{#if scanMode && req?.stream}
169+
<div class="saa-scanbar">
170+
<span class="saa-scan-label">scanning stream</span>
171+
<a class="saa-scan-url" href={req.stream.url} target="_blank" rel="noreferrer">{req.stream.url}</a>
172+
{#if req.stream.kind}<span class="saa-scan-kind">{req.stream.kind}</span>{/if}
173+
<button type="button" class="saa-fire" disabled={firing} onclick={scan}>
174+
{firing ? 'scanning…' : 'Re-scan'}
175+
</button>
176+
</div>
177+
{:else}
178+
<TermBar bind:term {firing} onfire={fire} />
179+
<ProviderPalette {connectors} bind:selected={selectedProvider} />
180+
{/if}
144181
{#if fireError}<div class="saa-error saa-fire-error">{fireError}</div>{/if}
145182
</header>
146183

apps/search-and-add/src/ResultRow.svelte

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
let added = $state(false);
1919
let error = $state<string | null>(null);
2020
21+
// Scan mode: the corpus already holds this URL — badge it, park the ➕.
22+
const known = $derived(result.known === true);
23+
2124
async function add() {
2225
adding = true;
2326
error = null;
@@ -44,18 +47,19 @@
4447
<div class="saa-row-main">
4548
<a class="saa-row-title" href={result.url} target="_blank" rel="noreferrer">{result.title}</a>
4649
<span class="saa-row-host">{host(result.url)}</span>
50+
{#if known}<span class="saa-known">in corpus</span>{/if}
4751
{#if result.published_date}<span class="saa-row-date">{result.published_date.slice(0, 10)}</span>{/if}
4852
{#if result.content}<p class="saa-row-snippet">{result.content.slice(0, 220)}</p>{/if}
4953
{#if error}<div class="saa-error">{error}</div>{/if}
5054
</div>
5155
<button
5256
type="button"
5357
class="saa-add"
54-
class:added
55-
disabled={adding || added}
58+
class:added={added || known}
59+
disabled={adding || added || known}
5660
onclick={add}
57-
title={added ? 'added to the entity' : 'add to the entity'}
61+
title={known ? 'already in the corpus' : added ? 'added to the entity' : 'add to the entity'}
5862
>
59-
{added ? '' : adding ? '' : '+'}
63+
{added || known ? '' : adding ? '' : '+'}
6064
</button>
6165
</li>

apps/search-and-add/src/app.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@
5050
.saa-add.added { border-color: var(--color-ok-text, #6ee7a8); color: var(--color-ok-text, #6ee7a8); cursor: default; }
5151
.saa-add:disabled:not(.added) { opacity: 0.6; cursor: default; }
5252

53+
/* scan mode (Phase 5) */
54+
.saa-scanbar { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.6rem; flex-wrap: wrap; }
55+
.saa-scan-label { font-size: 0.78rem; color: var(--color-text-muted, #9aa0aa); }
56+
.saa-scan-url { color: var(--color-link, #8ab4f8); font-size: 0.85rem; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60%; }
57+
.saa-scan-url:hover { text-decoration: underline; }
58+
.saa-scan-kind { font-size: 0.7rem; padding: 1px 6px; border-radius: 3px; background: var(--color-border, #2a2c33); color: var(--color-text-muted, #cfd3da); }
59+
.saa-scanbar .saa-fire { margin-left: auto; }
60+
.saa-known { font-size: 0.68rem; padding: 1px 6px; border-radius: 3px; margin-left: 0.5rem; background: var(--color-ok-bg, #173a26); color: var(--color-ok-text, #6ee7a8); }
61+
5362
.saa-empty { color: var(--color-text-muted, #9aa0aa); font-size: 0.85rem; }
5463
.saa-error { color: var(--color-error-text, #f3a3a3); font-size: 0.8rem; margin-top: 0.35rem; }
5564
.saa-fire-error { margin-top: 0.5rem; }

apps/search-and-add/src/lib/search-client.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,37 @@ export async function fireSearch(args: {
2121
return { provider: r.provider ?? 'unknown', results: r.results ?? [] };
2222
}
2323

24+
export async function scanStream(args: {
25+
org_slug: string;
26+
stream_url: string;
27+
stream_kind?: string;
28+
client: string;
29+
}): Promise<{ results: ConnectorResult[]; already_known: number }> {
30+
const r = (await workspace.invoke('organization.stream.scan', args)) as {
31+
ok: boolean;
32+
items?: {
33+
url: string;
34+
title: string;
35+
snippet: string;
36+
published_date: string | null;
37+
already_in_corpus: boolean;
38+
}[];
39+
meta?: { already_known: number };
40+
error?: string;
41+
};
42+
if (!r.ok) throw new Error(r.error || 'organization.stream.scan failed');
43+
return {
44+
results: (r.items ?? []).map((i) => ({
45+
url: i.url,
46+
title: i.title,
47+
content: i.snippet,
48+
published_date: i.published_date ?? undefined,
49+
known: i.already_in_corpus,
50+
})),
51+
already_known: r.meta?.already_known ?? 0,
52+
};
53+
}
54+
2455
export async function fetchConnectors(): Promise<ConnectorInfo[]> {
2556
const r = (await workspace.invoke('connectors.inventory', {})) as {
2657
ok: boolean;

apps/search-and-add/src/lib/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,21 @@ export type SearchRequestDetail = {
1212
target: 'links' | 'corpus' | 'streams';
1313
seed_term: string;
1414
intent?: string;
15+
// Phase 5 — scan mode: when present, the stream URL is scanned via
16+
// organization.stream.scan instead of a term search.
17+
stream?: { url: string; kind?: string };
1518
};
1619

1720
// Normalized result every connector returns (services/social-search
18-
// connectors/types.ts ConnectorResult, verbatim).
21+
// connectors/types.ts ConnectorResult, verbatim) — plus the scan mode's
22+
// already-in-corpus flag, absent on term-search results.
1923
export type ConnectorResult = {
2024
url: string;
2125
title: string;
2226
content: string;
2327
score?: number;
2428
published_date?: string;
29+
known?: boolean;
2530
};
2631

2732
// connectors.inventory wire shape — ConnectorRegistration minus `fire`.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
date_created: 2026-07-22
3+
date_modified: 2026-07-22
4+
title: "Stream-scan lands and Augment-from-DB is feature-complete — scan a pulse stream, badge the known, one-click the new"
5+
lede: "Spec step 8 ships as a mode of search-and-add: a per-stream Scan button on the org card fires the official-blog machinery with the stream URL as the authoritative index, dedups against content_items, and badges what the corpus already holds — proven live against The Aspen Institute's blog, flag-flip and all. All five phases of the spec are now implemented."
6+
publish: true
7+
authors:
8+
- Michael Staton
9+
augmented_with:
10+
- Claude Code on Claude Fable 5
11+
files_changed:
12+
- services/social-search/src/stream-scan.ts
13+
- services/social-search/src/server.ts
14+
- services/record-surrealdb-resolver/src/resolver.ts
15+
- services/record-surrealdb-resolver/src/handlers.ts
16+
- services/workspace/src/capabilities.ts
17+
- apps/org-workbench/src/AdditiveList.svelte
18+
- apps/org-workbench/src/OrgCard.svelte
19+
- apps/org-workbench/src/lib/types.ts
20+
- apps/org-workbench/src/app.css
21+
- apps/search-and-add/src/App.svelte
22+
- apps/search-and-add/src/ResultRow.svelte
23+
- apps/search-and-add/src/lib/search-client.ts
24+
- apps/search-and-add/src/lib/types.ts
25+
- apps/search-and-add/src/app.css
26+
- context-v/plans/Augment-From-DB-Phase-5-Stream-Scan-Mode.md
27+
- context-v/specs/Augment-From-DB-Flow.md
28+
tags:
29+
- Progress-Update
30+
- Augment-From-DB
31+
- Stream-Scan
32+
- Entity-Pulse
33+
- Feature-Complete
34+
---
35+
36+
# Stream-scan lands — and the flow the spec drew is all there
37+
38+
Phase 5 of [[../context-v/specs/Augment-From-DB-Flow.md]], and with it the spec goes **Shipped**: all five phases implemented in one day against a Signed-Off spec, each phase its own plan, commit, and proof.
39+
40+
## What shipped
41+
42+
**The cheap trick that made this a half-day, not a week:** `runOfficialBlogPack` already had the exact seam — `curated_index_urls`, where operator-curated index pages skip discovery entirely. A stream scan IS "the index is already curated: it's the stream URL." So `organization.stream.scan` is a thin wrapper: fire the blog machinery at the stream, then dedup via the new **`content.urls.check`** on record-surrealdb-resolver (social-search has no DB access by design — the cross-service NATS read follows the domains.ts → content-ingest precedent).
43+
44+
**On the org card:** `AdditiveList` grew an optional per-entry action, and the Pulse-streams list wires it as **scan** — one click dispatches a stream-carrying envelope through the same localStorage + event + navigate contract as every 🔍.
45+
46+
**In search-and-add:** a stream envelope flips the surface into scan mode — TermBar and palette give way to the stream URL + Re-scan; result rows carry an **"in corpus"** badge with the ➕ parked; fresh items one-click into `org_corpus` through the existing add path, and the flag flips on the next scan.
47+
48+
Social walls (LinkedIn/Facebook) ride the same call but stay experimental per the spec's non-goal — blog/RSS/newsroom is the dependable path, and it's the one proven.
49+
50+
## Proof
51+
52+
Live, end-to-end, against the stream Phase 2's proof seeded: scan of `aspeninstitute.org/blog/`**8 real posts with publish dates** (July 2026, fresh) · `organization.corpus.add` one of them · re-scan → `already_known: 1` and the added item's flag flipped to true. Full sweep green: svelte-check 0/0 on both remotes (one caught-and-fixed prop-destructure miss in AdditiveList — svelte-check earning its keep), all builds, three service typechecks, Phase 1 proof re-run 7/7.
53+
54+
## Where the flow stands
55+
56+
All five phases of [[../context-v/specs/Augment-From-DB-Flow.md]] are implemented and pushed as `attempt(augment-from-db, …, step1–5)`. Remaining before calling the *product* (not the code) done: the operator browser walk-throughs each plan names — the six-flow front door, the org card's ➕s, the 🔍→pairing→add loop, add-person with automatic affiliation, and the scan badge — plus the spec's parked open questions (fire-log persistence, pack-template seed terms, pinned-deploy remote URLs) for a future pass.

0 commit comments

Comments
 (0)