A partner runs one command and gets fifteen startups triaged against a written investment thesis - each with a one-page memo in which every claim traces back to a public URL.
The design goal is not "an LLM writes memos". It is auditability: every analytical claim cites an evidence ID, every evidence claim cites a source URL and carries a verbatim excerpt, the final recommendation is made by deterministic policy rather than by a model, and a whole run replays from persisted artifacts with no network and no API key.
| Browse the live run | demo/site/index.html - 15 companies, scored and written up. Preview: python3 -m http.server 8000 --directory demo/site |
| Read the shortlist | demo/ranking.md - thesis, thresholds, the triage queue |
| Read one memo | demo/memos/n8n-io.md - the best-evidenced candidate in the run |
| See one AI call end to end | demo/ai-trace/ - request, response, and what validation decided |
| How AI was used | docs/AI_WORKFLOW.md - decisions, constraints, failures |
| How it was built | worklog/ - nine chronological stage entries, written as it happened |
| Five-minute walkthrough | docs/WALKTHROUGH.md |
flowchart LR
Q[Seed query] --> S[Source]
S --> E[Enrich]
E --> V[Evidence]
V --> A[Analysis]
A --> P[Policy]
P --> M[Memos + UI]
classDef llm fill:#e9f0ec,stroke:#1f4d3a,stroke-width:2px,color:#16181a;
classDef det fill:#f4f1ea,stroke:#8a8579,color:#16181a;
class V,A llm;
class Q,S,E,P,M det;
Green is the only place a model runs. Evidence extracts source-backed claims;
Analysis scores seven rubric dimensions and writes the narrative. Everything else -
discovery, fetching, the total, the confidence, the recommendation, the memos and the site -
is deterministic Python. The model's own suggested recommendation is recorded next to the
binding call and never consulted; in the live run the two disagreed on 7 of the 15.
uv sync
export ANTHROPIC_API_KEY=... # required only for a live run
uv run vc-scout run \
--query "AI customer support and back-office automation for small businesses" \
--limit 15 \
--run-id ai-smb-ops-demo \
--provider anthropic \
--model claude-sonnet-5 \
--effort lowOne command runs the whole pipeline - source → enrich → evidence → analysis → memos → site - and prints a stage timeline, the recommendation counts and where everything landed. Roughly 15 candidates costs about 45 HTTP requests and 30 model calls.
uv run vc-scout demo --run-id offline-demoThe same orchestrator, the same stages, the same HTTP and Algolia clients - only the transport underneath them serves committed fixtures instead of the internet, and the provider is deterministic. It produces real memos and a real site in about a second.
Running run again on a finished run makes no network and no provider call: every stage
whose artifacts are still current is resumed. Rebuild one stage and everything derived from
it with --force-stage, which never touches anything upstream:
uv run vc-scout run ... --force-stage analysis # re-analyse, re-render, re-publish
uv run vc-scout run ... --stop-after evidence # stop early, then continue laterIf a run left a few analyses failed - a model returning the wrong shape, a transient provider error - retry only those, rather than paying for all fifteen again:
uv run vc-scout recover-analysis --run-id ai-smb-ops-demo --provider anthropicIt reads the full report, retries only the candidates recorded as failed, merges the results back into that report with its candidate order and every total recomputed, and rebuilds the memos and the site offline. Every analysis that already succeeded is left byte for byte as it was.
python3 -m http.server 8000 --directory outputs/runs/ai-smb-ops-demo/site
# then open http://127.0.0.1:8000/uv run vc-scout export-demo --run-id ai-smb-ops-demoWrites a self-contained demo/ directory - the site, the memos, the ranking, every
validated artifact, and one AI call end to end with what was sent, what came back and what
was checked. No raw HTML, no credentials, no absolute paths: the export refuses to write
rather than ship any of them.
Every stage is also its own command, which is how the pipeline was built and how it is debugged:
uv run vc-scout --help # full command surface
uv run vc-scout config # active rubric, thresholds and confidence policy
uv run vc-scout source --query "..." --run-id my-run
uv run vc-scout enrich --run-id my-run
uv run vc-scout analyze --run-id my-run --evidence-only
uv run vc-scout analyze --run-id my-run
uv run vc-scout recommend --run-id my-run
uv run vc-scout build-ui --run-id my-runA stage run this way carries no provenance fingerprint, so a later run reruns it rather
than trusting it. That is deliberate: an artifact whose input cannot be established is not
an artifact a memo should be built on.
| Command | Purpose | Status |
|---|---|---|
source |
Discover candidates from Hacker News | available |
enrich |
Fetch and extract public company pages | available |
analyze --evidence-only |
Extract source-grounded evidence | available |
analyze |
Score and apply the recommendation policy | available |
recover-analysis |
Retry only the candidates a run failed on | available |
recommend |
Write partner-ready memos and the ranking | available |
render |
Deprecated alias for recommend |
available |
build-ui |
Generate the static research site | available |
build-site |
Deprecated alias for build-ui |
available |
serve |
Serve a generated report locally | planned |
run |
Full pipeline end to end, with resume | available |
demo |
The whole pipeline offline, from committed fixtures | available |
export-demo |
Assemble a reviewer-ready demo/ directory |
available |
config |
Show the live rubric and thresholds | available |
vc-scout source searches the public Hacker News Algolia API
with a bounded, deterministic family of twelve queries:
| Variant | Tag | Words | Weight |
|---|---|---|---|
query-show-hn |
show_hn |
all required | 1.00 |
query-launch-hn |
launch_hn |
optional | 0.85 |
query-story |
story |
optional | 0.50 |
intent-smb … intent-ecommerce-retail (9) |
(show_hn,launch_hn) |
optional | 0.90–0.95 |
The nine intent-* variants pair the query's own AI-automation wording with a specific
buyer or workflow — SMB, small business, business operations, customer support, sales,
finance and accounting, scheduling, back office, ecommerce and retail. They exist because
a genuinely relevant product often describes its workflow without ever using the
operator's phrasing: a scheduling agent for salons will never say "SMB operations".
Only the first variant requires every query word. The rest relax word matching, and the recall that buys is paid for by the relevance gate below — not by hoping the query happens to match.
Every story is classified from its title, one-liner, hostname and URL path against three concept groups:
| Group | Signal | Weight |
|---|---|---|
| A | AI automation — ai, agent, agentic, automation, copilot, assistant, … |
0.40 |
| B | Business buyer — smb, small business, merchant, retailer, contractor, … |
0.25 |
| C | Operational workflow — invoicing, scheduling, customer support, payroll, back office, … |
0.35 |
relevance score = 0.40 x A + 0.25 x B + 0.35 x C (each group: min(matches, 2) / 2)
direct A and (B or C) an AI product with an identifiable buyer or workflow
adjacent A only an AI product naming neither
irrelevant no A not an AI product at all
A business tool with no AI signal is irrelevant, not direct: groups B and C qualify
an AI product, they do not substitute for one. Candidates classified irrelevant, or
scoring below the minimum relevance of 0.20, are discarded before anything is ranked.
Ranking is lexicographic, not a single weighted score:
1. relevance class direct before adjacent
2. relevance score
3. quality score 0.55 x engagement + 0.25 x recency + 0.20 x variant weight
engagement = log10(1 + points + 2 x comments) / log10(1 + 500), capped at 1.0
recency = 1.0 up to 30 days old, decaying linearly to 0.0 at 720 days
Engagement therefore orders candidates that are already equally relevant and can never
lift a generic agent-infrastructure launch above an on-topic workflow product. A single
composite score previously allowed exactly that; see D14 in docs/DECISIONS.md.
The shortlist is filled from directly relevant candidates first. Adjacent candidates are held to 30% of the shortlist while direct supply lasts, and may fill the remainder only once it runs out. The run never pads: if fewer defensible candidates exist than were requested, it returns what it found and reports the shortfall.
Every component, every matched term and every rejection is recorded in
source-report.json, so the shortlist can be recomputed and argued with by hand.
Discovery ranking is not investment scoring. It runs before any page is fetched, knows nothing about the thesis rubric, and is never read by the recommendation policy. A top-ranked candidate can still be recommended pass.
vc-scout enrich reads a bounded set of pages from each candidate's own website: the
homepage, the exact URL posted to Hacker News when it differs, and up to three internal
pages chosen deterministically by role (product, pricing, customers, about, team,
changelog, blog - at most one per role, same-origin only). Pages are deduplicated by final
URL and by content hash, reduced to readable text, and persisted with their fetch metadata
for replay.
No candidate is ever removed for having a thin or unreachable site. A company whose pages could not be read keeps an empty bundle with categorised failures, so the gap stays visible to the stages that judge it. Missing information is missing, not negative.
Every URL this stage touches came from third-party text, so all fetching goes through one hardened client:
- http and https only, on their default ports
- hostnames resolved before connecting; loopback, private, link-local, multicast, reserved and unspecified addresses are refused, including IPv4-mapped IPv6 forms
- redirects followed manually so every hop is revalidated, capped at 3
- explicit 5s connect and 15s read timeouts
- responses abandoned mid-stream at 2 MB rather than downloaded then measured
- only
text/htmlis parsed; text is capped at 20,000 characters per page robots.txthonoured; 401 and 403 respected, never worked around- a descriptive User-Agent, and no credential, cookie or authorization header ever sent
Persisted fetch metadata contains response facts only - requested URL, final URL, redirect chain, status, content type, content hash, byte count and timestamp. No request headers, cookies, environment values or credentials are logged or stored.
vc-scout analyze --evidence-only hands each company's own material to a language model
under a versioned prompt and writes back only what can be verified against that material.
The model is treated as an untrusted witness. It sees a bounded, per-candidate view - that company's Hacker News record and its extracted pages, nothing else. It never sees another candidate, a discovery rank, the rubric or the thesis. Structured output is obtained with forced tool use against a fixed JSON schema.
Nothing it returns is written until it has been checked:
| Check | Rejection |
|---|---|
Every cited source_id was supplied for this candidate |
unknown_source_reference |
| Every claim carries a supporting excerpt | schema_validation_failed |
| Every excerpt appears in the text of the source it is attached to | excerpt_not_found |
independently_supported is backed by two or more separate sources |
schema_validation_failed |
| No duplicate claims | schema_validation_failed |
Claim identifiers are derived — ev-<sha256(company_id, claim, sources)[:12]> — never
supplied by the model, so a claim cannot be given an identity it did not earn. Invalid
output earns exactly one retry carrying the validation errors back; a second failure is
recorded and the run continues.
Each claim carries two independent labels: verification_status
(company_claim / community_signal / independently_supported) and inference_status
(explicit / inferred). Absence is first-class: unknowns records what the sources did
not establish and conflicts retains sources that disagree. A company with no readable
website still gets a dossier — the gap becomes unknowns, never a negative claim.
Source pages are arbitrary text that any founder can edit. Two defences:
- System instructions live in a separate channel and contain nothing about any company.
Source text appears only in the user message, fenced in explicit
BEGIN/END UNTRUSTED SOURCE <id>markers and introduced as data, never instructions. - Validation makes compliance irrelevant. A model that obeyed an injected instruction to invent revenue would still have to produce an excerpt, and there is no excerpt — so nothing is written. This is the defence that is actually tested.
Every attempt persists a request and a response artifact under llm/, carrying the exact
bounded source content supplied, the prompt version and hash, the structured payload, the
validation result and errors, token usage, stop reason and latency. No credential,
header, cookie or absolute path is ever written — asserted by a test that greps every
artifact. A stored response can be re-validated without calling the provider.
vc-scout analyze reads the evidence dossiers - and only the dossiers. Raw pages, raw
Hacker News responses and the web are unreachable from this stage: what counts as evidence
was already decided and verified upstream.
What the model does: the narrative, a per-dimension assessment against the rubric, a thesis-fit verdict, risks, open questions, and an advisory recommendation.
What the model does not do: the total (recomputed in Python from its own components), the research confidence (computed from coverage facts), or the binding recommendation (made by deterministic policy). Its suggestion is recorded and compared, never obeyed.
The score measures the strength of the evidence-backed investment case, not the company's objective worth. Every dimension carries a status, and the status caps it:
| Status | Score ceiling | Meaning |
|---|---|---|
supported |
100% of maximum | The evidence backs this |
partially_supported |
70% | Some evidence, not enough |
contradicted |
100% | The evidence shows a problem — and must say what |
not_assessable |
50% | Nothing was found. Not a finding against the company |
not_assessable is deliberately neither forced to zero nor to the midpoint — the model
must choose and explain how the score reflects the uncertainty. scored_out_of reports how
many points were assessable, so a low total can be read correctly.
Computed deterministically after the model answers, from six coverage components with
bounded penalties for identity warnings, conflicts and unknowns. A zero-claim dossier
scores 0.0. The full formula and its thresholds are in D30 of docs/DECISIONS.md and are
reproduced in vc_scout.policy.compute_confidence.
The independently_supported label earns nothing on its own — only findings the analysis
explicitly names as corroborated count (D31).
Bands are 80-100 take a meeting, 65-79 watch, 0-64 pass. Then:
- a meeting also needs medium confidence, an identifiable product and buyer, no identity warning, and evidence in four dimensions;
- a pass band with more than three unassessable dimensions and low confidence becomes watch for insufficient evidence — unless the evidence positively shows a thesis mismatch, which may still pass;
- a zero-claim dossier becomes watch, never a fabricated score narrative;
- an unresolved cross-domain identity mismatch caps at watch;
- a missing website never forces a pass on its own.
Every guardrail that fires is named in the artifact, next to the band it moved from, the model's suggestion, and whether the two disagreed.
vc-scout recommend turns the stored artifacts into a partner-ready read. It makes no
provider call, needs no API key and touches no network — it renders what earlier stages
already validated.
uv run vc-scout recommend --run-id source-test
uv run vc-scout recommend --run-id source-test --force # re-render over existing outputoutputs/runs/source-test/
├── memos/<company_id>.md # one ~700-900 word memo per candidate
├── ranking.md # the reviewer's entry point
└── recommendation-report.json # what was rendered, and what could not be
Each memo opens with the call, the score and the confidence, then a snapshot table, why this call (the policy's own rationale, verbatim, plus every guardrail in plain language), a Team/Product/Market view, the seven-dimension scorecard, risks and open questions, the two-or-three things that would change the call, and a numbered source list:
# rulemesh.com
**Pass** · 40/100 · high confidence
**One-sentence call:** Pass: rulemesh.com scored 40/100 against the thesis rubric on
high-confidence research, with thesis fit recorded as adjacent - short of what a meeting needs.
...
| Pain and measurable ROI | 8 / 20 | Partially supported | Compliance is plausibly recurring… | [S1] [S2] |
...
**[S1]** Compliance Requirements Engineers Can Actually Implement | RuleMesh · company homepage · <https://rulemesh.com/> · observed 2026-08-23Three properties are worth knowing:
- Deterministic. Same artifacts, same bytes. Nothing here carries a generated timestamp, and every collection is sorted, so re-rendering is a check rather than a new opinion.
- Every citation resolves.
[S1]markers are numbered in reading order, each resolves to exactly one source entry, and every listed source is cited somewhere above. A source URL is rendered as its own link text, so a memo cannot show a label that points somewhere else. Internalev-/unk-identifiers never appear as a reader-facing citation. - Untrusted text stays text. Company names, page titles, model narrative and page excerpts all came off third-party pages. They are neutralised before rendering, so they can contribute words but never headings, tables, code blocks, HTML, images or links.
ranking.md carries the thesis, the rubric, the thresholds, the run summary and a table
ordered by call, then score, then confidence, then name. That ordering is a triage
queue, not a quality ranking, and the document says so: a watch that exists only because
the research came up short is not a claim that the company is better than one that was
passed on evidence. When no candidate reaches the meeting band, the ranking explains why
from the run's own counts rather than talking a candidate up to fill it.
vc-scout build-ui turns the same artifacts into a read-only site: a portfolio page and
one page per company. No provider call, no API key, no network, no build step, and no
runtime dependency of any kind - two files of hand-written CSS and JavaScript sit beside
the HTML and nothing else is fetched.
uv run vc-scout build-ui --run-id source-test
uv run vc-scout build-ui --run-id source-test --force # rebuild over an existing site
# preview it
python3 -m http.server 8000 --directory outputs/runs/source-test/site
# then open http://127.0.0.1:8000/outputs/runs/source-test/site/
├── index.html # the portfolio: summary, thesis, filters, table
├── companies/<company_id>.html # one page per company
├── assets/styles.css # one stylesheet, system fonts, no imports
├── assets/app.js # search, filter, sort. No framework, no network
└── ui-report.json # what was generated, and what could not be
The portfolio page carries the run summary, the thesis and thresholds, live search and filters (recommendation, confidence, thesis fit) with sorting, and a table that becomes stacked cards on a phone. Every row links to a company page carrying the decision header, the snapshot, why this call with the policy's rationale verbatim, the seven-dimension scorecard, the investment view, risks and open questions, what would change the call, the numbered sources, and a provenance panel. Company pages print cleanly.
Security posture. Every page declares
default-src 'none'; style-src 'self'; script-src 'self'; img-src 'self'; font-src 'self'; base-uri 'none'; form-action 'none'.
Jinja autoescaping is on and no untrusted string is ever marked safe. Every href is
validated - absolute http/https only, or a relative path this generator built - so a
javascript: or data: URL cannot become a link. There are no inline event handlers, no
inline styles, no remote images, no external scripts, stylesheets or fonts. The embedded
filter data is escaped so that no value can close the <script> element it sits in, and
the JavaScript writes only through textContent.
Accessibility. One <h1> per page and no skipped heading levels, labelled form
controls, scoped table headers, visible focus rings, external links marked with an icon and
rel="noopener noreferrer nofollow", prefers-reduced-motion respected, no horizontal
overflow at 375px, and a recommendation that is never carried by colour alone - each badge
pairs its colour with the word and a distinct glyph.
We invest in seed-stage, AI-native software companies that automate recurring, revenue-critical workflows for SMBs. The product should produce measurable value within 30 days, integrate into an existing system of record, and develop defensibility through proprietary workflow data, distribution, integrations or operational depth rather than relying only on model access.
| Dimension | Points |
|---|---|
| Pain and measurable ROI | 20 |
| Product wedge | 15 |
| Distribution | 15 |
| Defensibility | 15 |
| Team | 15 |
| Traction and freshness | 10 |
| Market and timing | 10 |
| Total | 100 |
80-100 take a meeting, 65-79 watch, 0-64 pass.
Research confidence is separate from the investment score. The score answers "how well does this fit the thesis?"; confidence answers "how much did we actually find out?". Missing information is recorded as unknown, never as a negative judgment - it leaves a dimension unscored and lowers confidence, and low confidence caps the recommendation at watch.
src/vc_scout/
├── pipeline.py one-command orchestration: order, resume, stop/continue
├── stages/ source · enrich · evidence · analysis · recommend · ui · recover · export
├── llm/ provider (raw httpx), compact schemas, the real validators, fake provider
├── models/ Pydantic artifact contracts, frozen, extra="forbid"
├── render/ Markdown memos and the HTML site, from shared view models
├── policy.py confidence and the binding recommendation. No model, no network
├── rubric.py the seven dimensions and their weights
├── thesis.py the thesis, versioned and content-hashed
├── assessment_policy.py what a source may be used to support, per dimension
└── prompts/ versioned prompt files, hashed into every artifact
Every artifact is a validated Pydantic document written atomically with sorted keys, so a
run diffs cleanly. Every path is built by RunStore and asserted to live inside the run
directory. Derived reports carry a fingerprint of the artifacts they were derived from, so
a resume can tell "already done" from "done against different inputs".
outputs/runs/<run-id>/
├── candidates.json source-report.json
├── extracted/<id>.json enrichment-report.json
├── evidence/<id>.json evidence-report.json
├── analyses/<id>.json analysis-report.json
├── memos/<id>.md ranking.md recommendation-report.json
├── site/ index.html · companies/<id>.html · assets/ · ui-report.json
├── llm/ every request and response, per attempt
├── raw/ fetched page bodies
└── run-report.json the whole run: stages, timings, tokens, versions
This is the property the whole pipeline exists to give you. Open
demo/memos/n8n-io.md and pick a sentence carrying a marker:
- The scorecard row for Product wedge cites
[S2]. - Sources at the foot of the memo resolves
[S2]to one entry: the page title, its role, the public URL and the date it was read - with the excerpt quoted beneath it. demo/artifacts/analyses/n8n-io.jsonshows whichev-claim IDs that row cited.demo/artifacts/evidence/n8n-io.jsonshows each of those claims with its verbatim excerpt and thesrc-source it came from.
Every marker in a memo resolves to exactly one source, and every listed source is cited somewhere above it. A statement resting only on a recorded gap is labelled Open question rather than left unattributed.
56 numbered decisions with their costs are in docs/DECISIONS.md. The
ones that shape the output most:
- The model never makes the call. The total is recomputed in Python from its own
components; confidence comes from coverage; the recommendation comes from
policy.py. - Absence of evidence is not evidence of weakness. A dimension the sources cannot reach
is
not_assessable, which caps what it may score and lowers confidence - it never becomes a negative finding, and the memos say so in those words. - Provenance caps what a source may conclude, not whether it may. A company's own page is good evidence of what its product does; it is not evidence of a result, an advantage or a scale.
- A conflict blocks the dimension it is about, not every dimension sharing a page with it.
Known limitations: the evidence in this run is thin and mostly company-authored, so no
candidate reached the meeting band; the rubric is uncalibrated against real outcomes;
prompt injection is mitigated rather than solved; sourcing depends on what Hacker News
happened to contain that week. docs/AI_WORKFLOW.md states these in full.
uv run pytest # 950+ tests, offline, no API key required
uv run ruff check .
uv run ruff format --check .
uv run mypy src # strict on src/The suite cannot reach the network: an autouse fixture blocks sockets and DNS and strips
every *_API_KEY from the environment, so a regression that reintroduced a live call fails
loudly rather than quietly costing money. Network stages are exercised through the
production clients over httpx.MockTransport; the LLM is exercised through a deterministic
fake that quotes only what it was given. CI runs the same four commands on Python 3.12.
docs/WALKTHROUGH.md is a timed outline:
| 0:00-0:30 | The problem, and the thesis being applied |
| 0:30-1:10 | One command, and where the models actually run |
| 1:10-2:30 | One startup from a Hacker News post to verified evidence |
| 2:30-3:30 | Analysis, the score, and the deterministic call |
| 3:30-4:15 | The dashboard and one memo |
| 4:15-5:00 | AI workflow, what broke, trade-offs |
docs/AI_WORKFLOW.md- how AI was used, and how its output is constraineddocs/DECISIONS.md- 56 design decisions with their costsdocs/PLAN.md- the plan agreed before implementation, including non-goalsdocs/WALKTHROUGH.md- the five-minute walkthrough outlineworklog/- nine chronological stage entries, written at each boundary
Built with AI assistance, disclosed in full in
docs/AI_WORKFLOW.md. Every commit is authored by the repository
owner; no commit carries an AI trailer.
No license provided. All rights reserved by the repository owner.