Upsert One Advisor -> Harper
This skill takes one advisor and writes sourced, deterministic Harper
data: Advisor, employment history, disclosures, sanctions, licenses,
outside business activities, team memberships, article mentions, and
field assertions.
It composes existing repo paths:
1. BrokerCheck search/fetch
bun run brokercheck -- --search-name "<legal name>" --max 10 --dry-run
bun run brokercheck -- --crd <CRD>
2. AdvisorHub coverage
search research/wpjson/ and research/articles/
write research/extractions/<wpId>.json
bun run load:extractions
3. Targeted public web research
add only quoted, source-backed soft fields
4. Verify
bun run verify or bun run verify:rest
BrokerCheck wins on regulatory facts. AdvisorHub wins on narrative and non-regulatory metrics. Web research fills only fields supported by a quote or reliable source URL.
Pre-flight
Use Bun and current scripts from package.json:
bun run build
bun run status
If local Harper is stopped, ask before bun run bootstrap. For
deployed Harper, set:
HDB_TARGET_URL=https://advisory-rankings-de.cody-swann-org.harperfabric.com
HDB_ADMIN_USERNAME=...
HDB_ADMIN_PASSWORD=...
src/lib/harper.ts uses HDB_TARGET_URL when present; otherwise it
falls back to the local Harper operations socket.
Parse Input
Required:
- legal name exactly as it appears in regulatory/source material
Useful disambiguators:
- CRD
- current or previous firm
- state
- career-start year
- first employer
If the input is ambiguous, show the parsed target and ask before writing regulatory data.
Resolve CRD
Skip this if the user provided a CRD.
bun run brokercheck -- --search-name "<legal name>" --max 10 --dry-run
Inspect the returned _source blocks:
ind_source_id- CRDind_firstname,ind_lastname,ind_middle_nameind_other_names[]- current and previous employment firm names
ind_bc_scope
Choose a CRD only when one candidate clearly matches the user's disambiguators. If zero or multiple candidates survive, stop and ask the user with a numbered list containing name, CRD, firms, state/scope when available. Never guess on regulatory identity.
Pull BrokerCheck
bun run brokercheck -- --crd <CRD>
This calls src/scripts/fetch_brokercheck.ts and writes through:
AdvisorBrokerCheckSnapshotEmploymentHistoryFirmDisclosureSanctionLicense
The state file research/brokercheck-state.json skips CRDs fetched in
the last 7 days. Use --force only when the user explicitly asks for a
fresh pull.
For existing rows missing CRDs, use:
bun run brokercheck -- --enrich --max 20
For firm/roster crawl work, use the orchestrator documented in
docs/brokercheck-spike.md:
bun run brokercheck:crawl -- --max-per-firm 50 --max-runtime-seconds 7200
Do not parallelize BrokerCheck fetches. The client rate limits requests and the state file makes repeated runs resumable.
Walk AdvisorHub Coverage
Search saved article JSON for the advisor's legal name, last name, and BrokerCheck AKAs:
rg -l -i "<legal name>|<last name>|<aka>" research/wpjson research/articles
If research/wpjson/ is empty or thin, run ingest-advisorhub first.
For each matching wpId, follow extract-advisorhub-articles:
bun run build
node dist/scripts/extract_helper.js show <wpId>
Read the article, write research/extractions/<wpId>.json, then load:
bun run load:extractions -- --wpid <wpId>
Use the same legal name as the BrokerCheck row. If loading creates a new advisor instead of matching the BrokerCheck-backed row, fix the extraction natural key before continuing.
Targeted Web Research
For missing soft fields, run one or two targeted web searches:
"<legal name>" "<current firm>" advisor bio"<legal name>" CFP CFA designation"<legal name>" Barron's "top advisors""<legal name>" "<current firm>" team"<legal name>" site:linkedin.com/in
Fetch only high-signal sources such as firm bios, public ranking pages, or press releases. Do not scrape LinkedIn pages; use snippets only for a profile URL.
Write only facts with traceable source text. For every fact:
- keep the exact source phrase or URL-backed phrase
- write a
FieldAssertion - use deterministic IDs from
src/lib/ids.ts - upsert through Harper, never insert ad hoc
If a fact is not source-backed, leave it blank.
Data Conventions
Advisor.preferredNameis the first-name form only, not the full display name. Example:JamesforC. James Taylor.- BrokerCheck disclosures/sanctions should not be overwritten by
AdvisorHub prose. Keep press claims as
FieldAssertionprovenance when they disagree with regulator data. - Use
sourceType: "brokercheck"for regulator facts and preserve BrokerCheck "as of" attribution throughBrokerCheckSnapshot.
Verify
Run the normal join checks:
bun run verify
Against deployed Harper:
bun run verify:rest
For a focused local SQL spot-check, build and call the shared SQL helper
from src/lib/harper.ts in a short Node one-liner or temporary script;
do not resurrect the old Python _harper.py helpers.
Report:
- name and resolved CRD
- BrokerCheck rows loaded or skipped
- AdvisorHub articles processed and mentions loaded
- soft fields added from web research
- ambiguous matches, skipped sources, or access failures
Idempotency
| Layer | Mechanism |
|---|---|
| BrokerCheck fetch | research/brokercheck-state.json skips recent CRDs unless --force is used. |
| IDs | src/lib/ids.ts uses UUIDv5 natural keys. |
| Harper writes | src/lib/harper.ts uses upsert operations. |
| Extraction files | extract_helper skips files already present in research/extractions/ or .loaded/. |
| Re-runs | Same source data resolves to the same IDs and updates existing rows. |
Automation Guidance
Use this skill for targeted advisor refresh Automations, not broad archive crawling. A safe recurring prompt should cap work, for example:
- run
bun run build - run
bun run brokercheck -- --enrich --max 20 - search saved AdvisorHub articles for newly enriched advisors only if context allows
- load any completed extractions
- run
bun run verifyorbun run verify:rest - report counts and ambiguities
For broad news ingestion, use ingest-advisorhub. For small batches of
article prose, use extract-advisorhub-articles. For multi-day roster
crawls, use bun run brokercheck:crawl with explicit caps and runtime
limits.
For scheduled public-web research, use the durable queue:
bun run research:advisors -- due --max 5 --stale-days 30 --json
bun run research:advisors -- record --advisor-id <id> --status no_new_data
Search only the due advisors, apply the upsert-advisor public-web
research rules, write source-backed facts with FieldAssertion, then
record success, no_new_data, ambiguous, or failed so the next
scheduled run can move on.