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

  • ctxgrd 0.61.0 or later.
  • A ctxgrd.toml in the repository root. If none exists, run ctxgrd pack add project-docs to create a baseline.

1. Add the pack

ctxgrd pack add research

This 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-run

Like 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:

ctxgrd

With 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 tune

The 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.typeRequired skeleton headings
academicMethods, Results
marketMethodology, Recommendations
deep-researchSummary, 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-research

5. 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