Linting deep-research reports
Keep every deep-research report in your repository evidence-honest – a sources section present, a limitations section at least nudged – without forcing frontmatter ceremony onto a genre that has none today.
What ctxgrd checks – and what it does not
ctxgrd checks two things on every report under docs/research/: does a
heading exist that reads as an evidence/sources section, and does a heading
exist that reads as a limitations/data-gaps section. Both checks are
contains-matches over a configurable synonym list, not exact heading text –
“Evidence appendix,” “Sources,” and “Works Cited” all satisfy the first check.
It does not check whether the evidence is any good. A report can name a
heading “Sources,” list nothing under it, and still pass – ctxgrd reads the
markdown envelope, not the claims inside it. It also does not require
Markdown links: deep-research tools cite with tokens
(citeturn0search1-style), not https:// URLs, so a rule demanding link
density would fail the entire genre. The load-bearing signal is structural
disclosure – did the author name where the evidence lives – not link count.
Prerequisites
ctxgrd0.61.0 or later.- A
ctxgrd.tomlin the repository root. If none exists, runctxgrd pack add project-docsto create a baseline.
1. Add the pack
ctxgrd pack add researchThis writes a [RESEARCH] block into ctxgrd.toml, path-claiming
docs/research/**. To preview what would be written without changing
anything:
ctxgrd pack add research --dry-runLike the guide and c4 packs, RESEARCH is id-less: the filename is the
report’s identity, no id: frontmatter required. Drop a report in and
ctxgrd lints it on the next run; a repository with no docs/research/
folder yet stays silent – adding the pack does not demand a report exist
before you’ve written one.
2. Write a report
Create a markdown file under docs/research/. No frontmatter is required –
open with a title and write the report the way your research tool already
produces it.
# Vector Database TCO for Production RAG, 2026
## Summary
Self-hosted pgvector on existing Postgres infrastructure undercuts managed
vector databases on raw compute cost at low query volume, but the gap closes
past roughly 50M vectors once operational headcount is priced in.
## Findings
- Managed offerings (Pinecone, Weaviate Cloud) bill per-pod or per-query and
include HNSW index tuning as part of the service.
- Self-hosted pgvector requires a team that already runs Postgres in
production -- the marginal cost is an extension, not a new system to
operate.
- Above roughly 50M vectors, self-hosted index rebuild time and the on-call
burden of a home-grown scaling story start to erode the savings self-hosting
showed at smaller scale.
- Hybrid setups (pgvector for the long tail, a managed service for the
latency-critical top queries) appeared in two of the four vendor
case studies reviewed, suggesting the choice is not binary in practice.
## Evidence and Sources
- Vendor pricing pages for Pinecone, Weaviate Cloud, and Qdrant Cloud
(accessed 2026-07).
- Postgres pgvector extension benchmarks (`github.com/pgvector/pgvector`,
README benchmark section).
- Two customer case studies published by managed-vector-DB vendors
describing hybrid architectures at scale.
## Limitations and Data Gaps
- Vendor-published benchmarks are not independently reproduced here --
treat the crossover point as directional, not a guaranteed threshold.
- Compute pricing was sampled once in July 2026; cloud list prices move
faster than this report will be revised.
- No case study in the sample discussed multi-region replication cost,
which likely shifts the crossover point further in either direction.Run the linter to confirm it is clean:
ctxgrdWith both an evidence section and a limitations section present, this report
produces zero diagnostics. No id:, no date:, no frontmatter at all –
that is the intended shape for a one-off research artifact, not a decision
record.
3. See what an evidence-poor report looks like
Delete the sources and limitations sections and the same report fires:
error[research.evidence]: research report has no evidence/sources section
--> docs/research/vector-database-tco.md:1:0
|
1 | # Vector Database TCO for Production RAG, 2026
| ^
|
help: add an evidence/sources section, e.g. `## Evidence appendix`
warning[research.evidence]: research report has no limitations/data-gaps section
--> docs/research/vector-database-tco.md:1:0
|
1 | # Vector Database TCO for Production RAG, 2026
| ^
|
help: add a limitations/data-gaps section, or set severity/gaps_headings to tuneThe asymmetry is deliberate. A sources section is standard across academic, market, and AI-report genres, so skipping it is an error. A dedicated limitations heading is good practice but not a universal convention – plenty of solid reports fold caveats into their discussion instead of giving them their own heading – so skipping it only warns by default. Both diagnostics point at the H1 line, since the report carries no per-section positions of its own to point at more precisely.
4. Opt one report into a genre skeleton (optional)
research.type is a frontmatter field, absent by default, that adds
genre-specific skeleton checks on top of the baseline. It changes nothing by
being absent – the report above already lints clean without it – and it can
only ever add findings, never remove one. There is no pressure to add it.
Here is the same report with research.type: academic added and nothing
else changed:
---
research:
type: academic
---
# Vector Database TCO for Production RAG, 2026
## Summary
...Run the linter again:
warning[research.evidence]: `research.type: academic` report is missing a `method` skeleton section
--> docs/research/vector-database-tco.md:6:0
help: add a heading containing `method` (the academic report skeleton)
warning[research.evidence]: `research.type: academic` report is missing a `result` skeleton section
--> docs/research/vector-database-tco.md:6:0
help: add a heading containing `result` (the academic report skeleton)Nothing about the baseline check changed – the evidence and limitations
sections still satisfy research.evidence on their own. Declaring
research.type: academic only added two new warnings, for the IMRaD
Methods and Results headings this report does not carry (it uses
“Findings” instead, an AI-report/market convention, not the academic one).
That is the point of the field: a strict opt-in for teams that want IMRaD-style rigor checked. It can never turn a failing report into a passing one, so an author optimizing for a clean lint has no reason to reach for it unless they actually want the stricter genre check. Each genre requires a different pair of headings:
research.type | Required skeleton headings |
|---|---|
academic | Methods, Results |
market | Methodology, Recommendations |
deep-research | Summary, Conclusion |
A value outside this vocabulary is an error, not a warning:
error[research.evidence]: `research.type: analysis` is not a valid research genre
help: set `research.type` to one of: academic, market, deep-research5. Keep research prompts out of docs/research/
docs/research/ is for reports, not the prompts that commission them. If a
file under docs/research/ looks like this –
# Research Task: Vector Database Vendor Landscape
## Role
You are a market analyst.
## Research Question
Which managed vector database vendors serve production RAG workloads at
enterprise scale?
## Required Deliverables
A comparison table with pricing, latency, and index-rebuild behavior.– research.evidence fires on it, because a prompt has no evidence section
either. That is correct: this file is a research prompt, not a report, and
belongs under docs/prompts/ claimed by the PROMPT namespace instead. Move
it and the diagnostic goes away because the file is no longer path-claimed by
RESEARCH at all – it isn’t a case of relaxing the rule, it’s filing the
document where it actually belongs.
6. Narrow the vocabulary
Both heading allowlists, and the gaps-section severity, are config, not
hardcoded. Edit ctxgrd.toml to fit how your team actually titles sections:
[RESEARCH."research.evidence"]
evidence_headings = ["evidence", "sources", "references", "appendix"]
gaps_headings = ["data gap", "limitation", "assumption", "caveat"]
severity = "warning"Set severity = "error" to make a missing limitations section block the
build instead of nudging. Set either list to [] to disable that half of
the rule entirely – for example, a team that never wants the gaps check can
set gaps_headings = [] and stop seeing the warning.
7. Lint and iterate
ctxgrd # human-readable output
ctxgrd --format json | jq '.diagnostics[] | select(.code == "research.evidence")'Exit codes follow the standard contract:
0– clean.1– diagnostics reported.2– kernel or config error.
Next steps
- Rule packs – the full built-in pack catalog.
- Configuring namespaces – path claims and allowed-values in depth.
- Getting started – install, apply a pack, lint your first record.