Corporate Venturing Toolkit — Implementation Guide

End-to-end, step-by-step process for analysing corporate venturing from 10-K filings and curated web sources — from document identification to the final portfolio dashboard. Each step lists the programs used and the files created.

15 Dimensions Windowed Extraction Ensemble Scoring (median + uncertainty) Curated-Web Augmentation Claude Sonnet 4.5 (pinned 20250929)

0 · Pipeline at a glance

TICKER │ ▼ (1) Document identification + fetch ........ cv_sec.py / sec_filing_cache.py / corporate_identifiers_2.py ▼ (2) Preprocess into narrative sections ...... cv_analyzer.preprocess_10k / parse_narrative_sections ▼ (3) Build RAG index ......................... cv_rag.build_index (BM25 + MiniLM embeddings) ▼ (4) Entity discovery ........................ cv_rag.discover_investment_entities (uses the index) ▼ (5) Windowed extraction (all 15 dims) ....... cv_analyzer._window_text + _run_batch_extraction ▼ (6) [optional] Curated-web augmentation ..... cv_url_scraper.py + analyze --with-scraped ▼ (7) Financial extraction + XBRL merge ....... cv_analyzer + cv_sec.merge_xbrl_into_financials ▼ (8) Ensemble scoring (median + uncertainty) . cv_analyzer._scoring_prompt + _aggregate_ensemble ▼ (9) Investment & funds extraction .......... cv_analyzer._extract_investment_and_funds ▼ (10) Persist ................................. outputs_v2/<TICKER>/analysis.json ▼ (11) Per-company visuals + ensemble report ... cv_visualizer.py + ensemble_report.py ▼ (12) Portfolio aggregation .................. cv_report.py + cv_portfolio.py ▼ PORTFOLIO: master_dataframe.csv · portfolio_dashboard.html · portfolio_report.docx

Orchestration entry point: cv_toolkit.py (all sub-commands). The batch driver run_batch_v2.sh runs steps 1–12 for the whole company list using the broader framework_v2/ rubrics, writing to outputs_v2/.

Prerequisites & one-time setup

conda activate cv_env
pip install -r requirements.txt
export TOKENIZERS_PARALLELISM=false        # silence HuggingFace fork warning

# API key — stored in a git-ignored .env at repo root (loaded by cv_config.load_dotenv())
echo 'ANTHROPIC_API_KEY=sk-ant-...' > .env

Build the scoring rubrics once (converts the framework definitions into scoring bands + key indicators, cached in .cv_cache/):

python cv_toolkit.py rubrics      # build outputs/rubrics.json from framework/*.txt   (cv_framework.py)
python cv_toolkit.py describe     # add plain-English brief_descriptions (Haiku)
# v2 rubrics (broader definitions) are built automatically by run_batch_v2.sh → outputs_v2/rubrics.json
ProgramRole in setupCreates
cv_config.pySingle source of truth: dimensions, colours, paths, model pin, windowing / ensemble / caching toggles; loads .env.
cv_framework.pyBuilds rubrics from framework/ & framework_v2/ definition files via Claude.outputs/rubrics.json, outputs_v2/rubrics.json

The 12 steps

1

Document identification & fetch

Resolve the ticker to an SEC entity, locate its latest 10-K, and download the complete filing plus structured financials. Everything is cached so repeat runs are instant.

Programs corporate_identifiers_2.py (ticker → CIK / company name lookup, 60 companies) · cv_sec.py (get_filing_text(), fetch_xbrl_financials()) · sec_filing_cache.py (EDGAR download, SQLite cache, HTML/SGML section extraction with TOC-skip)
Command python cv_toolkit.py preprocess --ticker GOOGL  (auto-downloads; no local file needed) — or fetch to only download
Input A ticker (e.g. GOOGL). Optionally a local 10-K via --file.
Creates sec_cache/sec_filings.sqlite (full SGML, keyed by CIK+accession) · outputs/<TICKER>_xbrl_financials.json (authoritative GAAP/CVC metrics)
2

Preprocess into narrative sections

Strip XBRL/HTML boilerplate and isolate the analytical text — Items 1, 1A, 2, 7, and all of Item 8 including the financial-statement footnotes. If the input is already a clean narrative, it is detected (_looks_like_clean_narrative) and used as-is to avoid lossy re-processing that would drop Item 8 notes.

Programs cv_analyzer.pypreprocess_10k(), parse_narrative_sections(), _extract_financial_section()
Input Cached SGML filing (Step 1).
Creates outputs/<TICKER>_narrative.txt (the clean full narrative, e.g. ~253K chars for GOOGL)
3

Build the RAG index

Split the narrative into ~1,000-char passages and build a hybrid retrieval index: BM25 + all-MiniLM-L6-v2 sentence embeddings. The index supports index.query() = BM25 (≈60 candidates) re-ranked by cosine, blended 60% BM25 / 40% cosine.

Scope of RAG: the index is used only in Step 4 (entity discovery). It does not select the evidence that gets scored — Steps 5/8 read the full document. RAG here is a focusing/retrieval tool, not a scorer.
Programs cv_rag.pybuild_index(), TenKIndex, split_passages()
Input The narrative text (+ financial section) from Step 2.
Creates In-memory index (not persisted); embedding model cached locally by sentence-transformers.
4

Entity discovery (the RAG consumer)

Identify the company's named funds, vehicles, and portfolio companies, in three stages:

  1. Stage 0 — curated context (not RAG): load known funds / portfolio companies and per-dimension dimension_hints from company_context.json.
  2. Stage 1 — RAG retrieval: for each investment-themed seed query, call index.query(seed_query, top_k=5); keep the top ~20 highest-scoring passages — i.e. RAG surfaces the investment-relevant slice of the filing.
  3. Stage 1b — pattern matching: regexes scan those passages for fund names, "formerly / through our X" patterns, portfolio companies, and vehicle language.
  4. Stage 2 — Claude refinement (only if <2 funds found): send the top passages to Claude for additional entities.
Programs cv_rag.pydiscover_investment_entities() · reads company_context.json
Output An entities dict: named_funds, portfolio_companies, investment_vehicles, curated_fund_descriptions, dimension_hints — injected as context into Steps 5, 6 & 9.
5

Windowed extraction (all 15 dimensions)

Tile the entire narrative into overlapping 60K-char windows (6K overlap) and scan every window for all 15 dimensions via Claude. Nothing is truncated; evidence (verbatim sentences, key terms, financial figures) is merged and de-duplicated across windows. A cleanup pass re-extracts any practice still showing zero evidence.

Programs cv_analyzer.py_window_text(), _run_batch_extraction(), _batch_extraction_prompt()
Inputs Full narrative + rubrics + entities context. Server-side prompt caching on the shared prompt prefix.
Creates outputs_v2/<TICKER>/extraction_checkpoint.json (the merged evidence agg) · API responses cached to .cv_cache/<md5>.txt
6

Curated-web augmentation

OPTIONAL

Pull curated CVC / Accelerator / Impact web pages and fold them into the evidence. Two parts:

  1. Scrape (scrape sub-command → wraps cv_url_scraper.py): reads the URL workbook, fetches each page (requests/httpx + BeautifulSoup), and a Claude pass extracts structured CVC/accelerator/impact facts.
  2. Merge (analyze --with-scraped): the scraped full text is run through the same windowed all-15-dimension extractor (so it informs every dimension incl. all 7 objectives) and merged into the 10-K evidence before scoring. The result is tagged scraped_evidence_used: true.
Programs cv_url_scraper.py (lazy-imported by cv_toolkit.py) · cv_analyzer._load_scraped_text(), _load_scraped_extracted()
Commands python cv_toolkit.py scrape --ticker GOOGL  then  analyze ... --with-scraped
Input 20260604_CV_urls_updated.xlsx (curated URLs per ticker, read fresh each run)
Creates scraped/<TICKER>/result.json · scraped/<TICKER>/<TICKER>_{CVC,Accelerator,Impact}.txt · cv_url_results.json (master)
7

Financial extraction + XBRL merge

One Claude pass extracts financial metrics from the financial section; then authoritative XBRL values (revenue, R&D, CVC portfolio value, buybacks, etc.) override the text extraction for all standard GAAP metrics.

Programs cv_analyzer.py (financial prompt) · cv_sec.merge_xbrl_into_financials()
Inputs Financial section text + outputs/<TICKER>_xbrl_financials.json (Step 1).
Output financials block (merged into analysis.json at Step 10).
8

Ensemble scoring (median + uncertainty)

All dimension evidence is sent together for scoring. With ENSEMBLE_SCORING_N > 1 (default 5), scoring runs N independent passes on the same evidence (temperature=0); the reported value per dimension is the median, with min/max/std as the uncertainty. Ranks are recomputed from medians, the P×O matrix cells are medianed, and the headline total is the median of per-pass totals (range stored; sum_of_medians kept separately). Each pass uses a distinct cache slot, so first run draws fresh samples and reruns reproduce them.

Programs cv_analyzer.py_scoring_prompt(), _aggregate_ensemble()
Output emphasis scores (0–100), ranks, descriptions, top quotes, rubric bands, P×O matrix, score_uncertainty per dimension.
Creates outputs_v2/<TICKER>/ensemble_scores.json (per-dimension samples + stats)
9

Investment & funds extraction

A dedicated synthesis call combines the merged evidence, the discovered fund descriptions, the scraper's structured output, and the financial figures into a structured investment_and_funds field: number of investments, fund name(s)/size(s), and investment preferences (check size, stage, geography, technology) — with supporting quotes.

Programs cv_analyzer.py_extract_investment_and_funds()
Output investment_and_funds block added to analysis.json.
10

Persist the analysis

All of the above is assembled and written as the single structured record for the company.

Programs cv_analyzer.analyze_company()
Creates outputs_v2/<TICKER>/analysis.json — scores, ranks, P×O matrix, evidence sentences, ensemble, score_uncertainty, investment_and_funds, investment_entities, financials, scraped_evidence_used.
11

Per-company visuals + ensemble report

Render the per-company charts. Two dashboard variants are produced when ensemble data is present.

Programs cv_visualizer.pygenerate_all_company_visuals() (P×O heatmap, dashboards, optional word clouds) · ensemble_report.py (table + error-bar figure)
Commands python cv_toolkit.py charts · python ensemble_report.py outputs_v2/<TICKER> · report for the .docx
Creates dashboard.png (plain) · dashboard_uncertainty.png (min–max error bars) · practice_x_objective_heatmap.png · ensemble_scores.png + ensemble_scores_table.csv/.md · <TICKER>_report.docx · word_clouds/ (only if --word-clouds)
12

Portfolio aggregation

Flatten every company's analysis.json into one table, then build the cross-company analytics and the self-contained interactive dashboard.

Programs cv_report.pybuild_summary_row() / build_master_csv() · cv_portfolio.pyrun_portfolio_analysis() · cv_visualizer.py (portfolio heatmaps)
Commands python cv_toolkit.py csv  then  portfolio-report
Creates outputs_v2/master_dataframe.csv · outputs_v2/portfolio_dashboard.html (zero-CDN) · outputs_v2/portfolio_report.docx · outputs_v2/portfolio_charts/

The 15-dimension framework

4 Practices4 Activities7 Objectives
Corporate Venture Capital · Accelerators and Incubators · Venture Building · Venture Clienting Mentorship · Business Services · Workspace · Events Risk Management · Market Expansion · Access to New Technology · Develop Business Ecosystems · Social and Environmental Impact · Developing Entrepreneurial Culture and Capabilities · Financial Return

Each dimension scores 0–100 (emphasis). Rubrics are conceptual guides, not vocabulary checklists — the model scores the underlying activity regardless of wording. framework/ holds detailed v1 definitions; framework_v2/ holds broader v1-paragraph definitions used by run_batch_v2.sh.

How scores are computed — LLM vs. coded algorithms

Key point: the LLM produces the 0–100 judgments; Python does the orchestration, aggregation, ranking, and the matrix fallback. There is no statistical/regression model mapping features → scores, and RAG/BM25 never scores anything (it only retrieves passages for entity discovery, Step 4).

A · Each dimension's emphasis score (0–100)

An LLM judgment, aggregated by code:

  1. Assemble (code)_scoring_prompt() packs, per dimension, the extracted evidence (verbatim sentences, key terms, financial figures), the rubric (scoring bands + brief description), and company context into one prompt.
  2. Score (LLM) — Claude (temperature=0) returns per dimension an emphasis_score 0–100 (+ rank, description, top_quote, rubric_band, rationale). The number is the model's holistic judgment, anchored by explicit rubric bands: 20–39 incidental · 40–59 generic/enterprise-scale · 60–79 explicit/named/quantified · 80–100 comprehensive/strategic with metrics. No arithmetic maps evidence → score.
  3. Ensemble (code) — the scoring call runs N times (default 5); _aggregate_ensemble() reports the per-dimension median (statistics.median) with min/max/std as uncertainty.
  4. Rank (code) — recomputed by sorting median scores within each group (practices 1–4, activities 1–4, objectives 1–7) — not taken from the LLM.
  5. Total (code) — headline = median of the per-pass totals; sum_of_medians also stored.

B · Practice/Activity → Objective heatmap (P×O matrix)

A hybrid: LLM-judged cells where evidence is strong, deterministic theoretical alignment everywhere else.

  1. LLM cells — the scoring call also returns practice_x_objective_matrix (8 practice/activity rows × 7 objectives, 0–100) = "how strongly each row serves each objective." The model typically fills only rows with strong evidence; ensemble medians these cells.
  2. Code fallback fill (cv_analyzer.py) — any row the LLM left empty/zero is filled from the rubric's fixed cross_objective_relevance map (high/medium/low/none70/40/20/0), scaled by the dimension's emphasis score:
    scale = 0.25 + 0.75 × (emphasis_score / 100)
    cell  = relevance_number × scale
    So a 0-scored dimension still shows its theoretical alignment at 25% intensity; an 80-scored one at ~85%. Rows the LLM scored keep the LLM values; the rest get this computation.

The cross_objective_relevance map is authored by the LLM once when rubrics are built (cv_framework.py), then frozen in rubrics.json and applied deterministically at scoring time.

C · Division of labour

LLM (Claude, temperature=0)Coded algorithms (Python, deterministic)
Evidence extraction — which sentences support each dimension (windowed passes) Document fetch / SGML-HTML parsing / section extraction (regex)
Dimension scores (0–100) — the core judgment Windowing — tiling text into overlapping blocks (_window_text)
P×O matrix cells it chooses to fill BM25 + cosine retrieval — entity discovery only (cv_rag)
Descriptions, top quotes, rationales; investment_and_funds synthesis; entity refinement Evidence merge/dedup across windows; ensemble aggregation (median/std)
Rubric authoring — bands, indicators, cross_objective_relevance (one-time) Ranks (sort), totals (median/sum), P×O fallback fill
XBRL merge (authoritative financials), caching (md5), all charts (matplotlib)

Bottom line: scoring and P×O alignment-where-evidence-exists are LLM judgments; aggregation (median across passes), ranking, totals, and the theoretical P×O fill for unscored rows are deterministic code.

Final outputs & files created

Caches (built once, reused)

PathWhat
sec_cache/sec_filings.sqliteDownloaded EDGAR filings (SGML), keyed by CIK + accession
.cv_cache/<md5>.txtOn-disk Claude response cache (exact reproducibility); rubric builds + every API call
outputs/<TICKER>_narrative.txt · _xbrl_financials.jsonPreprocessed narrative + structured financials (shared by v1 & v2)
outputs_v2/rubrics.jsonScoring rubrics (bands, indicators, brief descriptions)

Per company — outputs_v2/<TICKER>/

FileContents
analysis.jsonThe master record: scores, ranks, P×O matrix, evidence, ensemble, score_uncertainty, investment_and_funds, investment_entities, financials, scraped_evidence_used
dashboard.png / dashboard_uncertainty.pngPer-company dashboard — plain, and with min–max ensemble error bars
practice_x_objective_heatmap.pngPractice/activity → objective alignment matrix
ensemble_scores.json · .png · _table.csv/.mdEnsemble samples, error-bar figure, and table
<TICKER>_report.docxNarrative Word report
extraction_checkpoint.jsonCached merged evidence (resume aid)
word_clouds/Per-dimension word clouds (only if --word-clouds)

Curated web (optional) — scraped/<TICKER>/

FileContents
result.jsonSources checked, doc links, structured extraction, cv_analyzer_input block
<TICKER>_{CVC,Accelerator,Impact}.txtCleaned full text per source category (the merge input)

Portfolio — outputs_v2/

FileContents
master_dataframe.csvOne row per company — all dimension scores, ranks, descriptions, financials
portfolio_dashboard.htmlSelf-contained (zero-CDN) interactive cross-company dashboard
portfolio_report.docxPortfolio Word report (heatmaps, benchmarks, "what works")
portfolio_charts/Portfolio heatmap, score distributions, aggregate P×O matrix

Command quick reference

Single company (full chain)

# 1) fetch + preprocess (Steps 1–2)
python cv_toolkit.py preprocess --ticker GOOGL --output outputs

# 2) [optional] scrape curated web (Step 6a)
python cv_toolkit.py scrape --ticker GOOGL

# 3) analyze: Steps 3–11 (windowed extraction, ensemble scoring, investment_and_funds, visuals)
python cv_toolkit.py --output outputs_v2 --rubrics outputs_v2/rubrics.json --ensemble 5 \
    analyze --ticker GOOGL --company "Alphabet Inc." \
    --file outputs/GOOGL_narrative.txt --with-scraped --force

# 4) ensemble table + figure (Step 11)
python ensemble_report.py outputs_v2/GOOGL

# 5) portfolio aggregation (Step 12)
python cv_toolkit.py --output outputs_v2 --rubrics outputs_v2/rubrics.json csv
python cv_toolkit.py --output outputs_v2 --rubrics outputs_v2/rubrics.json portfolio-report

Whole portfolio (batch — runs Steps 1–12 for every company)

bash run_batch_v2.sh --force                 # all companies, v2 framework → outputs_v2/
bash run_batch_v2.sh --ticker GOOGL --force  # single company through the v2 batch
Key toggles (cv_config.py / CLI): ENSEMBLE_SCORING_N (--ensemble N, 1 = off) · GENERATE_WORD_CLOUDS (--word-clouds, off by default) · --with-scraped (off by default) · EXTRACT_WINDOW_CHARS / EXTRACT_OVERLAP_CHARS · ENABLE_PROMPT_CACHE · pinned CLAUDE_MODEL = claude-sonnet-4-5-20250929.
Determinism: temperature=0 is greedy but not bit-deterministic at the API level. Exact reproducibility comes from the .cv_cache response cache; the ensemble (Step 8) quantifies the residual scoring uncertainty as error bars.

Appendix — Future extensions (not implemented)

Status: design sketch only — captured for future consideration. The current pipeline scores via holistic LLM judgment (see "How scores are computed"). This appendix outlines an optional hybrid "LLM-as-feature-extractor + transparent coded scoring" alternative.

Core idea — separate perception from judgment

Today the LLM both reads the evidence and emits the 0–100 number (the judgment is opaque). The hybrid splits these:

Step 1 — LLM returns structured features, not a score

FeatureTypeMeaning
directnessdirect / indirect / noneactivity explicitly described vs. inferred
specificitynamed+quantified / named / generic / vaguenamed funds/programmes, $ figures present?
materialitystrategic / moderate / incidentalcentrality/scale to the business
evidence_countint (capped)# distinct supporting sentences
financials_presentbool / count$ figures attached

Categorical buckets are far more stable across runs than a free 0–100 number → less ensemble noise.

Step 2 — deterministic scoring function (weights in cv_config)

score = BASE[directness]
      + W_spec     * LEVEL[specificity]
      + W_evidence * min(evidence_count, CAP)/CAP
      + W_fin      * financials_present
score = clamp(score, 0, 100)

Every score then decomposes traceably, e.g. CVC = 72 = direct(40) + named+quantified(20) + evidence 8/cap(8) + financials(4).

Step 3 — blend knob (keeps current behaviour available)

final = α · formula_score + (1 − α) · llm_holistic_score

α=1 → fully transparent/reproducible; α=0 → today's behaviour; in between → hybrid. Plus an optional LLM "override flag" for genuine edge cases the formula misses.

Step 4 (optional) — calibrate the formula as an interpretable model

Replace hand-set weights with a small ordinal/linear model fit on a labelled sample (human scores, or current LLM scores as a starting target). LLM features → interpretable coefficients → score, making scoring a defensible, reportable model.

P×O matrix analog

The fallback fill already does this (relevance × emphasis-scale). The hybrid would make the whole matrix formula-based for consistency: cell = f(row emphasis, cross_objective_relevance, objective emphasis, evidence co-occurrence) — removing the "LLM cells vs. coded cells" split.

Trade-offs

GainsCosts / risks
Full traceability (score → named contributions)Must design + validate the feature set and weights
Reproducibility (deterministic formula; stable categorical features)Features still come from the LLM (perception not eliminated)
Tunability / calibration against ground truthA rigid formula can miss nuance — mitigated by the α blend
Comparability across companies (one scoring rule)Extra LLM schema + a small validation harness

Footprint when implemented: new extraction schema, a score_from_features() in cv_analyzer, weights in cv_config, keep the current LLM-score path behind the α blend, and a harness comparing formula vs. LLM scores.