A Bloomberg-specific profile of ui-json-capture: drive the logged-in browser at human pace and
keep the JSON the page already receives. Load browser-automation (port 9222,
mcp__chrome-devtools__*) and read ui-json-capture for the procedure and its Red Flags.
Why this route
Measured 2026-09-30 against SEC EFTS, WRDS web and wrds_sec_search: Bloomberg matches per
exhibit (option "Exhibits Only"), runs unstemmed (stemmed_search: false), tolerates
extraction glitches ("Non -Competition" still matched), and has Terms & Connectors: "phrase",
^"case-sensitive", ! and * wildcards, N/x, NP/x, S/, P/, ATLx(), ATMx().
Coverage from 1994; ~19.6M EDGAR documents.
Login
The browser is often signed out. Credentials are the 1Password item Bloomberg Law (UVA) (vault
"Shared with Agents"); handle them only inside a dispatched agent, never in the main thread.
- Fields:
input#username,input#password. A bare#usernamematches the<m-text-field>wrapper first. EnterInput.insertTextafter focusing; React ignores.value. - Enter does not submit. Click the button whose text is
Sign In; it lands onbloomberglaw.com/start.
Search
- Form:
https://www.bloomberglaw.com/start#advanced-search/features_edgar_all_search/features_edgar_all_search(Keywords; Apply To = Forms & Exhibits / Forms Only / Exhibits Only; Forms; Exhibits; Exhibit title; filer, CIK, company, location, SIC, file number). The favorite link may need one reload. - Install
scripts/01-install-hook.jsbefore clicking Search, so page 1 is captured.
Wire facts (observed, not for calling)
POST /product/blaw/api/v1/search/criteria, body{"criteria":{"model":"features_edgar_all_search","term":…,"keywords_apply_to":"edgar_exhibit","page_size":50,"page":N}}. Page 1 is the form submit; page N comes from the pager. Filter clicks hit the same endpoint with new criteria; the hook keys each page by criteria-minus-page, so check__cap.queriesholds exactly one query before assembling.- Response:
results_page.components.documents.items[](50 per page;id,titlelike"WOLFSPEED, INC.; Form 10-K; EX-10.21",date,metadatafiler,subtitlesfile number / filed / period / form,extractswith<mark>hits);.remote_counttotal (the UI shows "1000+");components.facets.items[](form family, form type, exhibit type, company, SIC) with counts. - Paging is an in-app route change (URL
/product/blaw/search/results/<criteria_id>, same document), so the hook persists. Next control:svg[data-testid="search-results-next-page"],disabled="true"on the last page; click it with a dispatchedMouseEvent.
Capture
scripts/01-install-hook.js and scripts/02-run-pager.js here; 03-status.js, 04-dump.js,
05-stop.js and assemble.py from ui-json-capture/scripts/ unchanged:
python3 ~/.claude/skills/workflows/skills/ui-json-capture/scripts/assemble.py \
--batches scratch/blaw --glob 'batch_*.json' --out data/raw/blaw_hits.jsonl \
--expect-total <remote_count> --page-size 50 --id-field id
From hits to EDGAR (accession, full text)
A hit carries no accession or CIK. scripts/resolve_exhibits.py resolves an assemble.py file:
uv run --script ${CLAUDE_SKILL_DIR}/scripts/resolve_exhibits.py --hits data/raw/blaw_hits.jsonl \
--out-dir data/processed/blaw --cache-dir data/raw/edgar_sgml \
--phrase "consulting agreement" --phrase "non-competition" # phrases optional
- Join on WRDS: keys go up as parameter arrays (pgdata is a hot standby: no TEMP tables) and
join server-side to
registrant(file no.),filing_view(date, form prefix) andwrds_forms(wrdsfname), so only ids and paths come back, neverfilingtext. - Files via rclone: raw submissions from
wrds:/wrds/sec/archivesbywrdsfname, cached; over 10,000 files it usesedgar.md's server-side tar.--ignore-checksumis deliberate: the default md5 check opens an ssh session per file on the login node, and exhausted it at 16 transfers. - Parse locally: one pass over the SGML
<DOCUMENT>blocks gives TYPE, DESCRIPTION, FILENAME and text for every hit, with no per-hit query.
hits_resolved.jsonl / .parquet: Bloomberg fields, match_status (unique / resolved_by_exhibit /
ambiguous / unmatched), resolved_by (type → exact title form → DESCRIPTION → extract text),
doc_match, accession, cik, filename, sec_url, text_path, chars, extract_found,
phrase_verification. Title quirks it handles: normalized form drops /A (the title keeps it);
EX-99: EX-99.1 is TYPE: DESCRIPTION; EX-10.6 2 is the DESCRIPTION of the second EX-10.6;
Rule 425 Communications = 425.
Measured 2026-10-01 on 295 hits: WRDS join 1–27 s; 277 submissions = 2.62 GB by rclone in 27 s;
parse + extract ~20 s. 294 resolved, 1 unmatched (CB/A: absent from the WRDS tables). Exhibit
text equals sec.gov's document minus its SGML header line in 275/275 checked.
Run a first pass on extracts alone; send only positives and unsure rows to a full-text pass.
Filters
Results-page facets are <label> elements (click the label, not the a11y checkbox node); each
click is a new query. Form type lands in criteria as
"facets":{"normalized_form_type":["Form 8-K"], "form_family":[], "display_exhibit_type":[], "cik":[], "sic":[]}.
A facet slice under 1,000 needs no date filter: capture it whole and filter locally. Date presets
sit under [data-testid="date-selector-preset-dropdown-button"] (Any, Single date, Date range,
Last 7/30 days, Last 12 months, Last 5 years) and land as e.g. "date_type":"last_5_years".
Verified 2026-10-01: three-phrase query, Exhibits Only, Form 8-K → 356 hits, 8 pages captured by
the pager in ~4 min, assemble.py exit 0 (no gaps, 356 unique ids).
Not yet verified
Whether paging continues past result 1,000; the CSV export's row cap; the field names for a custom date range.