LLM-native coverage-annotated file viewer.
covcat reads a source file and overlays code coverage data from your test suite, producing output in LNCAF (LLM-Native Coverage Annotation Format) --- a text format designed for AI consumption, not human IDE gutters. Every executable line gets a coverage gutter marker. An LLM reading covcat output goes from coverage-blind to coverage-aware in a single tool call.
<system-reminder>
COV: src/orders.ts | | covered . uncovered ? partial [B:x/y]=branches [FN+/-]=function
Collected: 2026-04-01 | lines: 5/12 (42%) | branches: 1/4 (25%)
</system-reminder>
[FN+] 1→function processOrder(order: Order): Result {
| 2→ const validated = validateOrder(order);
? 3→ if (!validated) { [branch: else never taken]
. 4→ throw new ValidationError('Invalid order');
. 5→ }
| 6→ const pricing = calculatePrice(order.items);
? 7→ if (pricing.discount > 0) { [branch: both paths uncovered]
. 8→ applyDiscount(pricing);
. 9→ } else {
. 10→ pricing.discount = 0;
. 11→ }
| 12→ return fulfillOrder(order, pricing);
13→}
An LLM reading this immediately knows: line 4 is untested (.), line 3 has a partial branch (?) where the else path is never taken, and lines 8--11 have zero coverage. Instead of guessing what needs testing, it can write "a test where validateOrder returns false" --- a concrete, actionable instruction derived from the annotation.
- Installation
- Quick Start
- LNCAF Format
- CLI Reference
- Daemon Mode
- Configuration
- Coverage Formats
- Architecture
- Claude SKILL Integration
- Development
- Research Background
- License
# Install from npm
npm install -g coverage-cat
# Or run without installing
npx coverage-cat src/utils.tsThe npm package is published as coverage-cat and installs the covcat binary.
Requires Node.js 18+.
# 1. Run your tests with coverage first
yarn test:coverage # or: npx vitest --coverage
# 2. View coverage-annotated source
covcat src/utils.ts
# 3. That's it. covcat auto-discovers coverage data.If you have coverage/coverage-final.json (Jest/Vitest) or coverage/lcov.info anywhere in your project tree, covcat finds it automatically.
LNCAF defines a single-character left-margin gutter on every executable line, with optional directional branch suffixes. Three tiers trade detail for token efficiency.
| Marker | Meaning |
|---|---|
| |
Line covered (executed at least once) |
. |
Line uncovered (0 executions) |
? |
Branch statement, at least one path untaken |
|
Non-executable line (blank, comment, closing brace) |
[FN+] |
Function entry, called |
[FN-] |
Function entry, never called |
Minimal. Single-char gutter only. Best for large files (800+ lines) or batch scans.
// cov: | covered . uncovered ? partial branch
| 1→export function add(a, b) {
| 2→ return a + b;
3→}
Gutter + function markers + directional branch suffixes. Best for interactive agentic workflows and test generation.
<system-reminder>
COV: src/math.ts | | covered . uncovered ? partial [B:x/y]=branches [FN+/-]=function
Collected: 2026-04-01 | lines: 6/8 (75%) | branches: 1/2 (50%)
</system-reminder>
[FN+] 1→export function divide(a: number, b: number): number {
? 2→ if (b === 0) { [branch: else never taken]
. 3→ return 0;
. 4→ }
| 5→ return a / b;
6→}
Hit counts + function-level gap summary. Best for single-function deep analysis or hot-path review.
<system-reminder>
COV: src/orders.ts | ...
Top gaps: L4-5, L10-11
</system-reminder>
[FN+:42] 1→function processOrder(order: Order): Result {
|:42 2→ const validated = validateOrder(order);
? 3→ if (!validated) { [branch: if-true never taken]
. 4→ throw new ValidationError('Invalid order');
The single highest-value feature. Instead of a bare ratio [B:1/2], covcat tells the LLM which branch to test:
| Pattern | Annotation |
|---|---|
if/else --- only if-true taken |
[branch: else never taken] |
if/else --- only else taken |
[branch: if-true never taken] |
switch --- 2 of 4 cases hit |
[branch: case 2 never taken, case 3 never taken] |
x ?? default --- non-null only |
[branch: alternate never taken] |
| All branches missed | [branch: both paths uncovered] |
When directional data is unavailable (e.g., LCOV), falls back to ratio: [branch: 1/2 taken].
Usage: covcat [options] [command] [file]
LLM-native coverage-annotated file viewer. Renders LNCAF format.
Arguments:
file Source file to annotate with coverage
Options:
-V, --version Output the version number
-f, --format <tier> Annotation tier: compact, standard, verbose
-c, --coverage <path> Explicit coverage file path
--no-branches Hide branch detail annotations
--hit-counts Show hit counts (verbose tier)
--no-daemon Skip daemon, always parse directly
-h, --help Display help
Commands:
daemon Manage the covcat daemon
# Auto-discover coverage, standard tier
covcat src/utils.ts
# Compact tier for a large file
covcat --format compact src/large-module.ts
# Verbose with hit counts
covcat --format verbose --hit-counts src/hot-path.ts
# Explicit coverage file
covcat --coverage coverage/lcov.info src/utils.ts
# Skip daemon, parse directly
covcat --no-daemon src/utils.tsFor fast repeated evaluation, covcat includes a background daemon that caches parsed coverage data and watches for changes.
# Start the daemon
covcat daemon start
# All subsequent covcat calls go through the daemon (<5ms cache hits)
covcat src/utils.ts # First call: parse + cache
covcat src/utils.ts # Second call: cache hit
# Check status
covcat daemon status
# covcat daemon: running
# PID: 12345
# Cache size: 3 entries
# Memory: 1.2 MB
# Watched: 1 files
# Stop when done
covcat daemon stop- The daemon listens on a Unix domain socket (
.covcat/daemon.sock) - When
covcat <file>runs, it checks if the daemon is available - If running: sends an annotate request over the socket, gets cached results
- If not running: falls back to direct pipeline (parse inline)
- The daemon watches coverage files with
fs.watchand invalidates cache on change
CLI ──> Unix Socket ──> DaemonServer
│
┌─────┴──────┐
│ LRU Cache │ (500 entries, 128MB)
│ │
│ fs.watch │ (auto-invalidation)
└────────────┘
Run in foreground for debugging:
covcat daemon start --foregroundCreate covcat.yml (or .covcat.yml, covcat.yaml) in your project root:
coverage:
# Annotation tier: compact | standard | verbose
defaultFormat: standard
# Show covered lines with | marker
showCoveredLines: true
# Show execution hit counts (verbose tier)
showHitCounts: false
# Show directional branch details
showBranchDetails: true
# Max token budget for annotations (0 = unlimited)
maxAnnotationTokensBudget: 0
# Staleness handling: warn-and-show | warn-and-hide | silent-hide | error
stalenessMode: warn-and-show
# Staleness check: mtime | linecount | hash
stalenessCheckLevel: mtime
# Coverage file discovery
discovery:
maxWalkDepth: 10
additionalCandidates: []
# Explicit path (overrides auto-discovery):
# coveragePath: coverage/lcov.info
# Daemon settings
daemon:
enabled: true
watchFiles: true
cache:
maxEntries: 500
maxMemoryMB: 128Settings resolve in order of precedence:
- CLI flags (
--format verbose) --- highest priority - covcat.yml (
defaultFormat: compact) - Built-in defaults (
standard)
covcat walks up from the source file's directory looking for config files, checking each of:
covcat.ymlcovcat.yaml.covcat.yml.covcat.yaml
First match wins.
covcat auto-discovers and parses coverage data. Format detection uses file extension + content sniffing (first 512 bytes).
| Format | Files | Ecosystem | Branch Quality |
|---|---|---|---|
| Istanbul JSON | coverage-final.json |
Jest, Vitest, NYC, c8 | Directional (if/switch/cond-expr) |
| LCOV | lcov.info, *.lcov |
Universal interchange | Ratio-only (opaque block IDs) |
covcat walks up to the project root (detected via package.json, go.mod, Cargo.toml, etc.) and probes:
coverage/coverage-final.json # Jest/Vitest default
coverage/coverage-summary.json # NYC default
.nyc_output/coverage-final.json
coverage/lcov.info
lcov.info
16 paths are checked across Istanbul, LCOV, Cobertura, JaCoCo, Go, llvm-cov, SimpleCov, and coverage.py ecosystems.
Adding a new format is one file implementing CoverageAdapter:
interface CoverageAdapter {
readonly format: CoverageFormat;
canParse(filePath: string, contentPreview: string): boolean;
parse(coverageFilePath: string, content: string): Promise<FileCoverageMap>;
}Register it in src/adapters/detect.ts and coverage for that format works automatically.
┌──────────────────────────────────────────────────────┐
│ CLI (commander) │
│ covcat <file> ─── daemon subcommands │
└──────────┬──────────────────────────────┬────────────┘
│ direct │ via socket
v v
┌──────────────────┐ ┌─────────────────────┐
│ Pipeline │ │ DaemonServer │
│ │ │ │
│ discovery │ │ LRU Cache (500) │
│ detect format │ │ fs.watch │
│ parse adapter │ │ Unix socket IPC │
│ render LNCAF │ │ │
└──────────────────┘ └─────────────────────┘
│ │
v v
┌──────────────────────────────────────────────────────┐
│ Adapters │
│ IstanbulAdapter LcovAdapter (pluggable) │
│ │ │ │
│ v v │
│ ┌─────────────────────────────┐ │
│ │ Unified Data Model │ │
│ │ LineCoverage, BranchData, │ │
│ │ FunctionEntry, FileSummary │ │
│ └─────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
│
v
┌──────────────────────────────────────────────────────┐
│ LNCAF Renderer │
│ Compact ──── Standard ──────── Verbose │
│ (|.? ) (+[FN+] +branch) (+hits +gaps) │
└──────────────────────────────────────────────────────┘
| Decision | Choice | Why |
|---|---|---|
| Annotation placement | Left-margin prefix | LLM attention primacy: prefix primes interpretation |
| Branch encoding | Directional text | "else never taken" > "1/2" for test generation |
| Covered line annotation | Single | char |
Asymmetric density: 1 token for covered, 5-8 for uncovered |
| Daemon IPC | Unix domain socket | Lowest latency, no port conflicts, automatic cleanup |
| Cache strategy | LRU with mtime staleness | Parse-once serve-many; sub-ms cache hits |
| Config validation | Zod schema | Type-safe, descriptive errors on invalid values |
src/
index.ts CLI entry point (commander)
cli/
pipeline.ts Annotation pipeline orchestrator
core/
types.ts LNCAF type system (32 types)
config.ts YAML config loader (zod)
discovery.ts Coverage file auto-discovery
adapters/
istanbul.ts Istanbul JSON parser
lcov.ts LCOV tracefile parser
detect.ts Format detection + adapter registry
renderer/
renderer.ts 3-tier LNCAF renderer
daemon/
cache.ts LRU coverage cache
server.ts Unix socket daemon + client
covcat ships with a Claude SKILL definition at skills/covcat/SKILL.md. When installed in a Claude Code project, it enables natural-language triggers:
- "show coverage for src/utils.ts"
- "what's untested in this file?"
- "coverage gaps in the auth module"
- "run covcat on the changed files"
The SKILL definition includes the full LNCAF gutter reference, usage examples, and configuration guidance so Claude can use covcat effectively without additional prompting.
# Clone and setup
git clone <repo-url> covcat && cd covcat
yarn install
# Build TypeScript
yarn build
# Run tests
yarn test
# Run tests with coverage
yarn test:coverage
# Coverage fitness score (composite metric)
./scripts/score.sh
# Run in dev mode (no build needed)
npx tsx src/index.ts --help
npx tsx src/index.ts --coverage test/fixtures/coverage-final.json test/fixtures/utils.tsTests use Vitest with real fixture data (no mocking of coverage adapters):
test/
adapters/
istanbul.test.ts Istanbul JSON parsing + branch descriptions
lcov.test.ts LCOV parsing + summary computation
renderer/
renderer.test.ts All 3 tiers + gutter markers + branch suffixes
fixtures/
coverage-final.json Istanbul JSON fixture (processOrder function)
sample.lcov LCOV fixture (add + divide functions)
utils.ts Source file matching Istanbul fixture
math.ts Source file matching LCOV fixture
The project uses a weighted composite score to track test coverage:
score = stmts_pct * 0.60 + branches_pct * 0.25 + fns_pct * 0.15
./scripts/score.sh # Human-readable
./scripts/score.sh --json # Machine-readable
./scripts/score.sh --gaps # Scores + LNCAF-annotated source for lowest-scoring modulecovcat is built on the LNCAF research corpus, a systematic analysis of LLM-native coverage representation:
- 13 source coverage formats analyzed (LCOV, Istanbul, Cobertura, JaCoCo, Go, llvm-cov, SimpleCov, coverage.py, Codecov, V8, OpenCover, Clover, dotCover)
- 17 AI-coverage tools surveyed (CoverUp, Qodo Cover, Codecov AI, SonarQube, GitHub Copilot, etc.)
- 7 annotation strategies evaluated on an 8-criterion weighted matrix
- 0 existing tools produce coverage-annotated output in the line-numbered text format LLMs read
The key finding: no existing tool fills the gap between IDE gutter markers (visual, invisible to LLMs) and raw coverage data files (separate from source, require mental mapping). LNCAF bridges this gap with inline annotation that adds ~800 tokens of overhead to a 500-line file --- a 5-7% increase that transforms an LLM from coverage-blind to coverage-aware.
CoverUp (arXiv:2403.16218) provides the empirical proof: inline line-tagging achieves 82% median coverage vs. 47% baseline. Preamble-only references fail. covcat extends this insight from excerpt-level to file-level operation.
See FINAL-SYNTHESIS.md for the complete specification.
MIT