trigsight
Enables verification of portfolio claims by resolving citations to exact supporting sentences at build time, providing read-only tools to list work, find evidence, check stack mentions, and read case studies while failing the build on any unverifiable claim.
README
trigsight
An AI assistant that cannot make a claim about my work it can't prove.
Every factual statement it produces is bound at build time to the exact sentence that supports it. Clicking a citation scrolls the source page to that sentence and highlights it. If a cited passage does not exist in its source document, the build fails — an unverifiable claim is not shippable.
<!-- SCREENCAST: Act 1 — a general AI search product miscites my own published work. Act 2 — the same question here, cited to the exact highlighted sentence. Act 3 — CI rejecting a build because one citation went stale. -->
Why this exists
Portfolio chatbots make claims about their author's competence. A reader has no way to check them, and good reason not to try: measured citation error rates for general AI search sit above 60%, and only 51.5% of generated sentences are fully supported by their cited sources. So the claims get discounted and the feature is decoration.
The usual fix is to ground the model in a corpus and hope. That helps with hallucination but not with verification — the reader still gets a link to a whole page and has to hunt for the sentence backing the specific claim.
This inverts the trust model. The model never writes a link. It names a passage; the build resolves it against an index of what the documents actually say, or refuses to ship.
Measured results
| Comparable portfolio | trigsight | |
|---|---|---|
| Lighthouse performance (mobile) | 62 | 100 |
| Accessibility · Best Practices · SEO | 100 · 100 · 100 | 100 · 100 · 100 |
| Largest Contentful Paint | 3.9 s | 1.9 s |
| Total Blocking Time | 1,540 ms | ~20 ms |
| Initial JavaScript (brotli) | 380.9 KB | 133.9 KB |
| WebGL scene | none | shipping |
| Citations verified against source | — | 34 / 34 |
2.8× less JavaScript with a 3D scene running. Both figures are real on-the-wire
brotli transfer from the deployed sites, measured by the same harness
(bench/baseline/measure-payload.sh), three runs each — see docs/05-results.md
for why measuring the two sides different ways overstated this as 3.4×.
Retrieval, 30 hand-written golden queries at k=5:
| Config | recall@5 | MRR |
|---|---|---|
| Lexical only (BM25) | 0.933 | 0.729 |
| Hybrid (BM25 + local stand-in, RRF k=60) | 0.967 | 0.831 |
| Hybrid (BM25 + real embeddings) | 0.967 | 0.889 |
Real embeddings are text-embedding-3-small at 1536 dims, cosine, via Upstash Vector.
Three identical runs. Note what changed: recall did not improve, ranking did. At this
corpus size real embeddings do not find more relevant chunks, they rank the ones they find
higher — which is the metric that matters, since only the top six reach the model.
How the guarantee works
flowchart TB
subgraph build["BUILD"]
MDX["content/*.mdx"] --> N["normalise: collapse whitespace, casefold"]
N --> IDX["passage index"]
IDX --> VER{"every cited passage present exactly once?"}
VER -->|no| FAIL["EXIT 1 — build fails"]
VER -->|yes| OK["allowlist.json"]
end
subgraph run["REQUEST"]
Q["question"] --> R["hybrid retrieve: BM25 + vector, RRF"]
R --> M["model"]
M -->|emits a cite token| RES{"in allowlist?"}
OK --> RES
RES -->|yes| CHIP["chip with text-fragment deep link"]
RES -->|no| DROP["dropped"]
end
The model has no mechanism for producing a URL, so it cannot produce a wrong one. Prompt instructions are a request; an absent capability is an invariant.
The part that was harder than it looks
A browser matching #:~:text= compares against rendered text, and each part of
the directive must sit inside a single element. Two consequences that cost real
debugging:
- Comparing against raw source fails 3 of 7 passages a browser matches fine — passages crossing a newline, containing collapsed whitespace, or differing in case. A verifier that produces false failures trains you to disable it.
- A passage spanning inline markup renders as separate DOM nodes and cannot be matched at all, even though flattened text contains it. Two of 34 citations were in this state: reported bound, unmatchable in practice. Caught only by fetching every built page and searching its text.
Both are now enforced, with regression tests. The verifier also requires each passage to appear exactly once, disambiguating with a prefix when it does not — otherwise the browser silently scrolls to the wrong occurrence, a correctness bug that looks like success.
Quickstart
npm ci
npm run dev # http://localhost:3000
No credentials needed. Without AI_GATEWAY_API_KEY the chat endpoint returns the
retrieved context instead of an answer, so retrieval is inspectable on its own.
Verify the claims yourself
npm run verify:citations # the gate: exits 1 on any unbound claim
npx tsx bench/citations/verify.ts --demo # watch it reject a fabricated passage
npm run eval:retrieval # recall@5 and MRR over the golden set
./bench/payload/measure.sh 3990 150 # initial JS against the budget
npm test # 91 tests
Every number above comes from one of those. bench/ is committed and never
gitignored — an uncommitted harness turns a real result into an unverifiable claim.
MCP server
POST /api/mcp — stateless Streamable HTTP, spec 2026-07-28. Four read-only
verification tools rather than description tools:
| Tool | Answers |
|---|---|
list_work |
What is documented, with metrics flagged verified or not |
find_evidence |
What passages support a claim — or plainly that none do |
check_stack |
Whether a technology is discussed in prose or merely listed in a stack |
read_work |
Full text of one case study |
find_evidence originally confirmed "quantum cryptography research on ion traps". A
vector leg is nearest-neighbour search: it always returns its closest chunks however
unrelated. An agent consuming that would repeat a fabricated credential. It now
requires genuine lexical overlap, calibrated to 8/10 true positives and 0/6 false
positives against hand-written claim sets.
Limitations
Stated plainly, because a limitations section that reads as marketing is worthless.
- The margin over lexical-only is one query wide. At 34 chunks and 30 golden queries, one query is worth 0.033 of recall — so the 0.034 recall gap between lexical and hybrid is a single query. Do not read the table as proving hybrid superior in general. It shows hybrid is not worse and ranks better on this corpus.
- A prediction of mine was wrong, and the reason is instructive. I predicted real
embeddings would fix the one missed query. They did not. Probing the vector leg in
isolation showed the correct chunk never enters its top 5 — because that chunk is 1,340
characters covering five topics, and the answer is one clause 871 characters in. Its
single vector is an average of five ideas. That is a chunking problem, not an embedding
problem, and no model fixes it. Recorded rather than patched, because changing the
chunker now would invalidate the comparison the table exists to make.
See
bench/retrieval/results/real-embeddings-2026-08-21.md. - Without credentials the vector leg degrades to a deterministic stand-in that is not a semantic model. Retrieval still answers via BM25, but the quality is genuinely lower — the harness names the backend on every run so a number can never be quoted without it.
- Text fragments are fragile by design. Rewording a cited sentence breaks its link. That is the point of the build gate — you cannot ship the breakage — but it does mean editing prose sometimes means updating a citation.
- Citations must sit inside one block element. A claim best supported by text spanning a table row and a paragraph cannot be cited as a single passage.
- The corpus is small (6 documents, 34 chunks). Retrieval numbers here should not be read as predictions for a large corpus.
- No streaming citation resolution. Chips render after a message completes, not mid-stream.
Stack
Next.js 16.3.1 · React 19.2.8 (pinned — R3F 9 peers >=19 <19.3) · TypeScript ·
Tailwind v4.3.3 · Velite + Zod · three 0.185.1 + R3F 9.7.0 (pinned) ·
Vercel AI Gateway
Licence
MIT — see LICENSE.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.