URL auto-detector ships clickable links for socials / helpful_links / official_updates_index_urls; Per-Client-Privacy exploration lands; reach-edu becomes the first client submodule
Three coupled drops in the same session. First, the URL auto-detector + clickable rendering plan dropped earlier today implemented end-to-end in Record Collector — `apps/record-collector/src/logic/format.ts` grew a `FieldShape` discriminated union (`empty | scalar | scalar_url | url_list | json`) that picks by VALUE-shape never field-name, plus a `UrlEntry` extractor that surfaces `url + chip (from pack_id) + label (from label or display_name)` from `Array<{ url, ... }>` payloads. The structured-value branch in `App.svelte` switches on `shape.kind`: `url_list` renders as a vertical flex of clickable `<a target="_blank" rel="noopener noreferrer">` entries showing hostname+path (truncated at 60 chars with full URL in `title=`) so the operator can verify links resolve without leaving Record Collector. Auxiliary metadata (display_name, confidence, source_metadata, response_id, accepted_at, ...) drops from the rendered view but stays on the row and round-trips through CSV. Read-only by design; the per-entry remove affordance lives in a sibling plan, not this one. The keyed-each was unkeyed mid-implementation when `(entry.url)` was identified as a Svelte 5 duplicate-key crash risk against any row whose URL array has repeats — for read-only lists, positional iteration is the right call. Second, a Response Reviewer drill-through audit on the live response-store revealed 1,982 responses with 1,109 (56%) from the three Entity-Pulse OfficialPulse packs at a combined 3-accept rate — `official-blog-pack` 99.5% reject, `official-social-posts-pack` 100% reject, `official-pressrelease-pack` 100% reject — confirming the operator's 'tons of junk links' read of the surface. Domain breakdown showed 65% of pressrelease responses come from `news.google.com` (the google-news-rss connector with weak entity-name binding); 45% of social-posts responses come from `youtube.com` (weakly bound). The packs with strong identity binding — wikipedia (matches article title to entity), linkedin (returns entity's own page) — reject at the normal 73-82% rate for a triage workflow. Third, the operator's response to the junk-volume reframed the next-feature direction entirely: STOP triaging URLs one-at-a-time in Response Reviewer; START treating those URLs as a seed list for a Jina.ai content-ingestion pipeline that writes deduped markdown per funder; build a per-client content corpus that powers cross-funder fundraising-strategy synthesis and per-funder outreach customization. That direction surfaced the broader ai-labs architectural question the operator named explicitly — when does the single-operator-on-localhost posture stop scaling, what stack do we reach for, and how do we architect today so the move is cheap when it comes? — and produced `context-v/explorations/Per-Client-Privacy-and-the-Path-Off-Local.md` (v0.0.0.1, Draft) mapping five candidate stacks (defer-everything, per-client Railway single-tenant, multi-tenant SaaS shape, hybrid posture, managed BaaS) across five axes (repo topology, storage substrate, identity/auth, multi-tenant data model, sensitivity constraints), naming six decision-forcing functions, and five architecture choices that cost almost nothing today but preserve cheap optionality for every path-flip. Operator signed off on Path D — hybrid posture — and committed to start local: `lossless-group/augment-reach-edu` created as a private repo (default README); `clients/reach-edu` registered as a git submodule pointing at it. The reach-edu corpus + operational data + Jina ingest pipeline land in that submodule in follow-up sessions; the augment-it parent stays on the docker-local posture until one of the named forcing functions actually fires.
URL auto-detector, Per-Client-Privacy exploration, reach-edu submodule
What shipped
1. URL auto-detector + clickable rendering in Record Collector
Implements the plan from earlier today
(context-v/plans/URL-Auto-Detector-and-Clickable-Rendering-for-List-Fields.md,
v0.0.0.1).
apps/record-collector/src/logic/format.ts extends the formatter with
a FieldShape discriminated union:
type FieldShape =
| { kind: 'empty' }
| { kind: 'scalar'; text: string }
| { kind: 'scalar_url'; url: string }
| { kind: 'url_list'; entries: UrlEntry[] }
| { kind: 'json'; text: string };Shape detection is purely value-based (never field-name-based) so the
affordance applies uniformly to any future URL column. isLikelyUrl
matches http:// / https:// / // prefixes after trim — render
hint, not validation; rejects bare paths so prose isn't accidentally
linkified.
isStringUrlArray returns true iff every element of an array is a
URL-string. isObjectUrlArray returns true iff every element is a
non-null object with a url property passing isLikelyUrl. The
latter handles socials (Array<SocialProfile>) and helpful_links
(Array<HelpfulLink>) cleanly. objectArrayToEntries surfaces the
URL plus an optional chip (from pack_id, with -pack suffix
stripped to match the existing .socials chip-row) and an optional
label (from label if non-empty, otherwise display_name). All
other auxiliary metadata (socials_id, confidence, snippet,
source_metadata, response_id, accepted_at, link_id, note,
source, added_at) is intentionally dropped from view — it's
preserved on the row and round-trips through CSV via the existing
download.ts path.
apps/record-collector/src/App.svelte:444 swaps the structured-value
branch from if (isStructured) to a switch on shape.kind:
url_listrenders as a vertical flex of clickable links. Each entry shows the optional chip, optional label, and an<a target="_blank" rel="noopener noreferrer">whose text isdisplayUrl(entry.url)— strip the scheme + leadingwww., clip at 60 chars with ellipsis — and whosetitle=holds the full URL for hover.jsonkeeps the existing read-only structured branch with the(empty [])/(empty {})placeholder for empty arrays / objects.Everything else (
empty,scalar,scalar_url) flows through the existing contenteditable scalar branch.scalar_urlrouting throughscalarpreserves edit-on-click for single-URL fields; the optional icon-button affordance forscalar_urlstays out of scope per the plan.
apps/record-collector/src/app.css:288 adds .field-value-urls
(flex column container, theme --color-field background, 10em
max-height with overflow), .field-value-url (per-entry row),
.field-value-url-chip (uppercase pill, theme --color-border
background, --color-text-muted text), .field-value-url-label
(compact, muted, max-width 12em with ellipsis), .field-value-url-link
(theme accent color, ellipsis-truncated, underline on hover,
accent-2 on visited).
The keyed-each {#each shape.entries as entry (entry.url)} was
unkeyed mid-implementation when entry.url was identified as a
Svelte 5 duplicate-key crash risk against any row whose URL array
has repeats (helpful_links accumulated across sessions, socials
whose pack_ids resolve to the same profile URL). For a read-only
list with no reordering, positional iteration is the right call. The
prior bug it explained: rows pane went blank while the surrounding
template (filename, "Augment this Set" panel, sidebar) kept
rendering — classic single-iteration error in a keyed {#each}
within the parent rows {#each}.
2. Per-Client Privacy and the Path Off Local — exploration
context-v/explorations/Per-Client-Privacy-and-the-Path-Off-Local.md
(v0.0.0.1, Draft) maps the option space surfaced by two coupled
forcing functions:
Funder content corpus needs storage. The Jina-pull-to-markdown pipeline the operator wants must be private per client. reach-edu and Laerdal share little; both treat the material as highly sensitive.
ai-labs broader posture is hitting the local-only ceiling. Active collaborator joining, client asking for login soon, funder corpus expanding the data substantially.
Five candidate stacks across five axes (repo topology, storage substrate, identity/auth, multi-tenant data model, sensitivity):
Path A — Defer everything. Per-client repos + markdown only + operator-only auth + per-client isolation by deployment. Zero new infra cost; costs migration work later.
Path B — Per-client Railway single-tenant. Each client gets their own Railway project. Magic-link auth. Application work: auth flow, deployment automation, per-user routing.
Path C — Multi-tenant SaaS shape. Single hosted deployment,
client_idon every table, full identity provider, RLS. Most durable; most upfront work.Path D — Hybrid posture (RECOMMENDED, signed off). Per-client repo for corpus + per-client Chroma collection now; augment-it docker-local for operational data, stashed inside the per-client repo so it's the migration source when the flip comes; auth + hosting deferred until a forcing function fires.
Path E — Managed BaaS. Powabase flagged expensive; Supabase / Convex / PocketBase-cloud worth a survey if Path C ever becomes target.
Six decision-forcing functions named (watch for these, don't guess timing): collaborator needs remote access to operational data, a client wants a login URL, two collaborators want to write the same client simultaneously, a client demands audit logs / SSO, corpus crosses a size threshold, a client asks for SOC 2 / GDPR posture.
Five architecture choices that cost almost nothing today and
preserve cheap optionality: funder corpus as markdown files,
operational data in per-client repo (not docker volume only),
client_id as a first-class field in every new artifact, Chroma
collections as the retrieval primitive, auth shape declared but
not implemented.
3. reach-edu — first client submodule
Path D's first concrete action.
lossless-group/augment-reach-edu— private repo created on GitHub viagh repo create --private --add-readme. Default README; description points back to this exploration..gitmodulesregisters the new submodule:[submodule "clients/reach-edu"] path = clients/reach-edu url = https://github.com/lossless-group/augment-reach-edu.gitclients/reach-edu/— submodule entry pointing at the initial commit of the new repo.
The corpus directory shape, the Jina ingest pipeline, the per-client Chroma collection, and the operational-data co-location stash all land in that submodule in follow-up sessions. The augment-it parent stays on the docker-local posture; nothing in this commit changes how the existing stack runs.
Sibling client submodules — clients/laerdal (against a
lossless-group/augment-laerdal-edu or similar private repo) — drop
in beside reach-edu whenever that client work begins. The
clients/ directory is gitignored from the lens of the running
augment-it app; only the submodule pointers are tracked at the
parent level.
Verified
tsc --noEmitclean acrossapps/record-collectorafter the format.ts shape additions and the App.svelte switch.lossless-group/augment-reach-eduexists at https://github.com/lossless-group/augment-reach-edu (private).git submodule statusshowsclients/reach-eduregistered against the new repo's initial commit.
What's next on this branch
Out of scope for this commit; named so the next session has a landing spot:
Funder corpus shape inside
clients/reach-edu/—corpus/directory, per-funder subdirectory,<YYYY-MM-DD>_<title-slug>.mdnaming, YAML frontmatter (source URL, fetched_at, original published date if extractable, content hash, entity_id).content-ingestservice — Jina.ai integration. Consume a URL list (seed from the entity-pulse pack responses already sitting in response-store), callr.jina.ai/<url>per entry, dedupe by URL canonicalization → content hash → title+date, write markdown.reach-edu-funder-corpusChroma collection — paralleling the existing four (context-vigilance-corpus,lossless-changelog,claude-code-sessions,claude-code-tool-traces). Ingest script extendscontext-vigilance-kit/scripts/.Per-entry remove affordance in Record Collector — the curate half of [[OfficialPulse-URLs-Appear-as-Junk-in-Promoted-Versions]]. Sibling plan to today's URL-Auto-Detector plan; not started.
Pre-promote review step in PromoteBar — same OfficialPulse issue's §Prevention #2.
Triage / clear the 1,109 OfficialPulse-pack responses sitting in response-store — likely via the existing
response.delete_allcapability filtered by pack_id, or by hand once the Jina ingest pipeline has consumed the seed URLs.
Each gets its own context-v artifact (spec / plan / issue) when the work begins.
