Skip to main content

Carta

Test PyPI Python License: MIT

Carta

Maps, connects, and remembers your documentation.

Carta is a Claude Code plugin that keeps your project docs honest — auditing for contradictions, embedding reference material into a searchable knowledge base, and surfacing the right context exactly when you need it.


The problem (or: how this got built)

Fast-moving projects accumulate documentation debt quietly. You write a spec. An AI agent writes a dozen more files based on it. The spec changes. Three weeks later, four different documents describe the same API endpoint four different ways, and nobody — human or AI — knows which one is right.

This problem gets worse the more you lean on AI agents to help you work. Agents are only as good as the context they can see, and when your docs/ folder is a fog of contradictions and stale frontmatter, you're giving your agent a map that leads off a cliff.

Carta started as a happy accident. While working through a project with a lot of PDFs, datasheets, and fast-changing markdown — the kind of repo where the hardware changes on Thursday and the docs are still describing Wednesday — we built a small structural scanner to flag stale and broken cross-references. Then we added a semantic pass. Then a vector store. Then a /doc-search skill so Claude could query the embedded knowledge directly.

At some point we looked at what we had and realized: this is a thing. It works. It's small, it runs locally, it requires no new services beyond what an LLM-augmented developer already has running. So we generalized it.


What Carta does

Three things, tightly integrated:

1. Audit

A two-pass system that runs on a schedule or on demand:

  • Structural scanner (zero LLM calls) — detects stale docs, broken related: links, homeless markdown files, and orphaned content. Runs fast, runs often.
  • Semantic audit (Claude) — reads the scanner output and checks changed doc pairs for contradictions: version numbers, API endpoints, config values, whatever matters in your domain. Writes a rolling docs/AUDIT_REPORT.md with stable AUDIT-NNN issue IDs that persist across runs.

2. Embed

Ingests your reference material — PDFs, datasheets, manuals, audio transcripts — into a local Qdrant vector store via Ollama. Generates spec_summary blocks for dense documents so the audit agent can cross-reference them without re-reading 200 pages.

Natural language recall over everything that's been embedded. Ask Claude what the docs say about rate limiting, authentication flows, power supply constraints, sample naming conventions — whatever's in your knowledge base — and get cited answers back.


Retrieval quality

Search is hybrid (dense + BM25 with Reciprocal Rank Fusion) by default, with an optional ColPali visual layer for image-heavy PDF pages. Measured on a real technical-docs corpus (~160 markdown docs + 214 datasheet PDFs, local models — nomic-embed-text + Qdrant/bm25):

Text retrieval — markdown eval, 20 queries:

Pipeline recall@5 MRR
Dense only (cosine) 0.550 0.402
Hybrid (BM25 + dense, RRF) 0.700 0.546

On an expanded 62-query set over the same corpus (adds datasheet, supplier, and patent reference docs): hybrid alone scores 0.790 / 0.641, and the LLM reranker (qwen3.5:9b, candidate pool 40) lifts it to 0.871 / 0.778 — with rerank: applied on 61/62 queries confirming the reranker actually ran on every scored query but one.

Visual retrieval — datasheet eval, 14 queries:

Pipeline recall@5 MRR
Text / OCR only 0.500 0.429
+ ColPali visual (two-pass) 0.857 0.589

The datasheet set includes 6 "visual-only" queries whose answer lives on a diagram, package drawing, or derating curve that text search structurally can't reach — ColPali lifts those from 0/6 to 5/6. Text and visual hits are fused by rank (RRF), so the visual layer never crowds out text results.

These are one project's eval sets, not a public benchmark — they show the delta each layer adds on real technical docs, not an absolute SOTA claim.

When search.rerank.enabled is true, carta eval also prints rerank: applied on N/M queries — and fails (exit 1) if the reranker ran on zero queries, so a silent fail-open (wrong model name, Ollama down, reasoning-model misconfig) can never masquerade as a reranked result.

Eval sets that don't saturate. Once recall@5 nears 1.0 it can no longer detect a regression or measure an improvement, so carta eval also reports recall@1 / recall@3 and supports two optional per-query fields: reject: — plausible-but-wrong docs (a sibling part's datasheet, a superseded plan); ranking one above the gold is counted as a hard-negative violation — and tags:, which break recall@k and MRR down per label (e.g. pdf-deep, vocab-mismatch). --json PATH writes the full metrics, per-query ranks included, for A/B tooling. See carta/eval/datasets/example.yaml.

Comparing against public benchmarks. To position Carta against standard suites:

  • ViDoRe v1/v2 (nDCG@5) — the visual-document-retrieval benchmark ColPali/ColQwen2 are evaluated on; the most direct check of Carta's visual layer.
  • BEIR / MTEB retrieval / RTEB (nDCG@10) — standard text-retrieval generalization.
  • FreshStack — hard RAG over technical docs + code; closest in spirit to Carta's use case.

Good fits

Carta shines in projects where:

  • Docs outnumber the people who maintain them. Research repos, hardware projects, API platforms — anywhere the documentation surface area is large relative to the team.
  • AI agents are generating or editing docs. Agents don't track contradictions between files. Carta does.
  • Reference material lives outside version control. PDFs, datasheets, vendor manuals, meeting transcripts — Carta pulls them into the same queryable knowledge base as your markdown.
  • The project changes fast. Embedded firmware, evolving APIs, active research — anything where a doc written last Tuesday might already be wrong by Friday.

Less useful for: simple single-repo projects with a handful of docs, or projects where the docs are already the source of truth and rarely change.


Quickstart

Version history: CHANGELOG.md. Install (pipx, venv, PATH): docs/install.md.

Add the Carta marketplace to your ~/.claude/settings.json:

{
  "extraKnownMarketplaces": {
    "carta-cc": {
      "source": {
        "source": "github",
        "repo": "Ian-q/Carta"
      }
    }
  }
}

Then install and enable the carta-cc plugin via /plugins in Claude Code. That's it — hooks and skills are registered automatically. Run /carta-init in any project to bootstrap Carta there.

CLI install (pip / uvx / curl)

For use without the Claude Code plugin, or if you want the carta command available directly:

# One-shot (no install required)
uvx --from carta-cc carta init

# Install as a CLI tool (recommended on macOS)
pipx install carta-cc
carta init

# Install directly (may require --user or a venv on macOS/PEP 668 systems)
python3 -m pip install carta-cc
carta init

# Or via curl
curl -fsSL https://raw.githubusercontent.com/Ian-q/Carta/main/carta/install/install.sh | bash

See docs/install.md for pipx vs venv, PATH, PlatformIO conflicts, and --pip-args syntax.


Setup (5 minutes)

Prerequisites:

# 1. Qdrant — run with persistence so collections survive restarts
docker run -d -p 6333:6333 -v ~/.carta/qdrant_storage:/qdrant/storage --name qdrant qdrant/qdrant

# 2. Ollama — install from ollama.ai, then pull required models
ollama pull nomic-embed-text   # text embeddings
ollama pull qwen3.5:0.8b       # only if you set proactive_recall.judge_backend: ollama
ollama pull qwen3-vl:8b        # vision describer for PDFs with figures/diagrams (needs Ollama >= 0.12.7)
ollama pull glm-ocr            # OCR for text/tables on image-heavy PDF pages

Both services are optional if you only want structural audit without embedding or search. See docs/install.md for the full setup walkthrough and carta doctor to verify your environment.

After init:

  1. Edit .carta/config.yaml — set your project_name, docs_root, and excluded_paths
  2. Add frontmatter to a few key docs:
---
related:
  - CLAUDE.md
  - docs/api/endpoints.md
last_reviewed: 2026-03-20
---
  1. Run your first audit: /doc-audit in Claude Code (or carta scan)
  2. Embed your reference PDFs: /doc-embed (drop files into docs/reference/)
  3. Query: /doc-search what does the docs say about authentication?

Skills

Skill What it does
/carta-init Bootstrap Carta in a new project (generates .carta/config.yaml)
/doc-audit Structural + semantic audit, generates docs/AUDIT_REPORT.md
/doc-embed Ingest PDFs, manuals, and audio transcripts into Qdrant. Supports .pdf, .md, .csv, .xlsx (xlsx needs the spreadsheet extra: pipx inject carta-cc openpyxl)
/doc-search Natural language search over the embedded knowledge base
carta focus / carta_focus Deep retrieval scoped to one file: page-anchored passages, an outline (omit query), and table/figure pages as images. Two-step partner to /doc-search (locate → go deep)
carta remember / carta_remember Save a curated project note (quirk / bug-note / helpful-note) as a repo markdown file and embed it. --about <file> records which file the note is about
carta impact / carta_impact Reverse lookup: given the files you changed (or a git range), the docs that name them or that git history shows changing with them, ranked and labelled, with the lines to re-read. No model, no Qdrant, no Ollama

Which audit command?

"Audit" spans several distinct workflows, and it's easy to reach for the wrong one:

You want to… Use Output
Find structural doc issues (stale/broken related:, homeless/orphaned docs) carta scan (no LLM) or the /doc-audit skill (structural + semantic) .carta/scan-results.json / docs/AUDIT_REPORT.md
Check embedded-data integrity (orphaned sidecars, damaged/duplicate points) carta audit JSON report
Diagnose the environment (Qdrant/Ollama/models) carta doctor (+ --fix) stdout
Measure retrieval quality carta eval scores
Find the docs that describe the files you changed carta impact <path>... · carta impact --diff ranked docs + evidence lines (--json)
Find docs that name a file which was deleted or renamed carta scan (missing_path_mention findings) .carta/scan-results.json
Reconcile CLAUDE.md against the docs (detect superseded sections, draft corrections) carta claude-md check (report JSON) · carta claude-md record (stamp sync sidecar) · /claude-md-sync skill JSON findings

Configuration

All settings live in .carta/config.yaml (generated by carta init from the template). Key fields:

project_name: my-project           # namespaces your Qdrant collections
qdrant_url: http://localhost:6333   # required — where Qdrant is running
docs_root: docs/
stale_threshold_days: 30
contradiction_types:
  - version numbers
  - API endpoints
  - configuration values
  # add domain-specific ones: pin numbers, CAN IDs, SQL table names, etc.
anchor_doc: CLAUDE.md              # fallback comparison anchor
modules:
  doc_embed: true                  # set false to skip embed layer
  doc_search: true                 # set false to skip search
embed:
  ollama_model: nomic-embed-text:latest

Search reranking

Hybrid retrieval (dense + BM25, RRF-fused) is the default. An optional second-stage reranker reorders the candidate pool for better top-k precision:

search:
  rerank:
    enabled: true
    backend: llm            # cross-encoder | llm
    model: BAAI/bge-reranker-base   # backend=cross-encoder (fastembed, local ONNX)
    llm_model: qwen3.5:0.8b         # backend=llm: one listwise Ollama call per search
    llm_timeout_s: 20
    candidate_pool: 40      # docs fetched before reranking (default 30; ~40 suits the llm backend)
  • cross-encoder — fastembed bge-reranker-base, no Ollama, fast.

  • llm — sends the query + candidate excerpts to a local Ollama model in a single call and reorders by its judgment. Fail-open: any error/timeout falls back to the fused order. Reasoning models are handled (think is disabled so the answer lands in the reply, not the thinking stream), and the parser tolerates a JSON array wrapped in stray prose.

    Model strength matters a lot. On a real technical-docs corpus (62-query eval), a strong reranker (qwen3.5:9b) lifted recall@5 0.790 → 0.871 / MRR 0.641 → 0.778. The small default (qwen3.5:0.8b) is fast but can degrade ranking on harder corpora — use it for low latency, and a 9b-class model when retrieval quality is the priority (at higher per-query cost).

Reranking applies to explicit searches (carta search, the MCP carta_search tool, carta eval). The proactive-recall hook never reranks (and never loads ColPali) — it fires on every prompt and blocks submission, so it always uses the fast fused order; its gray-zone judge handles relevance filtering.

The gray-zone judge is a cross-encoder, not an LLM. When retrieval is neither clearly on topic nor measurably irrelevant, the hook asks one question — does any of this answer the prompt — and a cross-encoder answers exactly that question directly. The default (Xenova/ms-marco-MiniLM-L-6-v2, via the fastembed dependency Carta already ships for reranking) costs ~10 ms warm and ~0.5 s cold in a fresh process, needs no Ollama, and cannot be derailed by a reasoning model deciding to think. Measured on 229 labelled passages from an 84-query eval corpus: it separates relevant passages from unrelated chunks at AUC 0.915, where the binary qwen3.5:2b LLM judge it replaces reached 0.78. At the shipped threshold it passes 80% of genuinely relevant passages while admitting under 5% of unrelated ones.

proactive_recall.judge_threshold (default -10.4) is a raw, model-specific logit — not a probability. Re-derive it for a different judge_model, or for your own corpus:

python -m carta.hook.eval.calibrate chunks http://localhost:6333 myproj_doc chunks.jsonl
python -m carta.hook.eval.calibrate judge .carta/eval/myproj.yaml chunks.jsonl

judge_backend defaults to auto: the cross-encoder when fastembed is installed (it ships in the carta-cc[hybrid] extra), the yes/no LLM judge when it is not — so a plain install still judges instead of going quiet. Force either with judge_backend: crossenc / ollama (the latter needs proactive_recall.ollama_model pulled). carta doctor reports whether the judge model is cached and --fix fetches it, so the hook never downloads one while blocking a prompt.

Search budget. For the same reason, the hook runs under a wall-clock budget — proactive_recall.search_timeout_s, default 3 s — covering the query embed and the Qdrant queries together. If it expires the hook stays silent and the prompt proceeds immediately.

This matters most with a remote Qdrant. A dead localhost backend refuses instantly, so the underlying 60 s embed timeout never binds; a dead remote peer drops packets with no RST, so without a budget every prompt would stall for the full timeout. Only the hook is bounded — carta search, MCP and carta eval keep their generous timeouts, since a slow search there is fine and a slow prompt is not.

Hypothetical answers (HyDE)

A short question and the passage that answers it often share almost no words — especially when the answer is buried deep in a long PDF. Give the search a hypothetical answer and it blends that into the semantic match (the keyword lane still uses the query):

carta search "why do cached pages sometimes show old prices" \
  --hypothetical "Cache entries are invalidated on write, but CDN copies expire only after their TTL."

The MCP carta_search tool takes the same hypothetical argument, and its description tells Claude to write one; the /doc-search skill does too. The passage only has to be shaped like the answer — it does not need to be correct. On an 84-query eval built not to saturate (deep-PDF, vocabulary-mismatch and design-rationale questions), Claude-written hypotheticals lifted MRR 0.46 → 0.70 and recall@5 0.54 → 0.77 on the MCP path, and MRR 0.55 → 0.65 on the hybrid CLI path. Carta never writes the hypothetical itself: a local qwen3.5:9b writer did not help, and the caller that benefits is already a frontier model. Full measurement in the design spec.

Graph-aware retrieval (opt-in)

An optional pre-rerank stage walks the related: frontmatter graph (undirected, 1 hop) from the top hits and promotes graph-adjacent documents into the rerank candidate pool, so a relevant doc that ranks too deep to be seen can still surface. Fail-open, and off by default:

search:
  graph:
    enabled: false      # opt-in
    hops: 1
    seed_count: 10      # top fused hits that seed the walk
    candidate_depth: 50 # deep-fetch size when enabled

The benefit is realized through the reranker (it never displaces the top fused hits on its own). It was measured neutral on a corpus where a strong reranker already floats in-pool docs; it is most likely to help corpora with a rich, well-linked related: graph and relevant docs that rank deep. The companion related: resolver (id/path normalization) and the carta scan noncanonical_related check ship regardless and feed link-graph cleanup.

The graph arm: which docs describe this file?

Search answers "which passage is about this?". The graph arm answers the reverse question, the one you have while editing code: "I am changing these files; which docs describe them?"

It works from what docs already say. A doc that writes src/parser/frame.py in backticks, links to it, or shows it in a code block has declared a relation to that file. Carta extracts each such mention as an edge (doc, target, line, and the line's text as evidence) and stores the edges in one SQLite file, .carta/graph/edges.db. No model is involved: no embeddings, no Qdrant, no Ollama. The same repository always gives the same edges, and the store is a cache you can delete and rebuild.

carta graph build      # incremental by content hash; --force re-extracts everything
carta graph status     # counts, digest, and whether the store is stale (never builds)
$ carta graph build
Graph: 3 docs, 10 edges (3 re-extracted, 0 unchanged)

Then ask about paths, or about a git range (carta impact brings the store up to date first, so graph build is optional):

carta impact src/parser/checksum.py src/store/db.py   # these paths
carta impact src/parser/                              # everything under a directory
carta impact --diff                                   # <default-branch>...HEAD
carta impact --diff main..HEAD --json                 # any range, machine-readable
$ carta impact --diff
2 docs mention 4 changed paths (3 docs, 10 edges)
Range: main...HEAD

 1. docs/design/parser.md  score 5.119789
      src/parser/old_frame.py  exact, named by 1 doc
        L3: The framing rules live in `src/parser/old_frame.py`; see `parse_frame()`.
        L5: See [the frame source](../../src/parser/old_frame.py) for the state table.
      src/parser/checksum.py  exact, named by 1 doc
        L4: The checksum is `src/parser/checksum.py`, and the old path was `src/parser/legacy.py`.
      src/parser/legacy.py  exact, named by 1 doc
        L4: The checksum is `src/parser/checksum.py`, and the old path was `src/parser/legacy.py`.
 2. docs/overview.md  [already in this diff]  score 1.654053
      src/parser/checksum.py  via src/parser/ (dir), named by 1 doc
        L3: Everything under `src/parser/` and `src/store/` is covered by the parser design.
      src/parser/frame.py  via src/parser/ (dir), named by 1 doc
        L3: Everything under `src/parser/` and `src/store/` is covered by the parser design.
      src/parser/legacy.py  via src/parser/ (dir), named by 1 doc
        L3: Everything under `src/parser/` and `src/store/` is covered by the parser design.
      src/parser/old_frame.py  via src/parser/ (dir), named by 1 doc
        L3: Everything under `src/parser/` and `src/store/` is covered by the parser design.

Skipped: docs/overview.md (doc)

(The range here renames old_frame.py to frame.py, deletes legacy.py and edits checksum.py and docs/overview.md.)

Each result carries the lines that made the edge, so you can go straight to the sentence that may now be wrong. Rarer targets count for more: a file named by 2 docs is more telling than one named by 60. For any one changed path, a doc that names the exact file scores above one that names its directory (a directory mention is worth half per level above the file). A directory or glob mention is counted once per target, growing only as 1 + ln(n) with the n changed files it covers, so on a large change a doc naming the directory can still pass a doc naming one of the files. Deleted and renamed paths are found too: when a range deletes a path (or you ask about a path that is no longer on disk), the docs are read against the path set as it was before the deletion, so a doc that named the file by a partial path, a bare filename, a glob or a link still comes back. That is the case where the doc most needs an edit. With --diff, docs the range already edits are marked [already in this diff], and docs changed in the range are left out of the query unless you pass --include-docs. Changed paths that no doc names are listed at the end (No doc mentions: ...). Other flags: --top N, --min-score X, --kinds, --no-build (reads the store as it is and never writes; carta graph status likewise, and both report a damaged store instead of replacing it). The same query is available to agents as the carta_impact MCP tool, which returns the --json object.

carta scan uses the same edges to report a doc that names a path which no longer exists (missing_path_mention). It fires only when Carta can show the path was real: git history deleted or renamed it (the finding then names the new path), or it is a markdown link pointing into the repository. A path that never existed, a URL, a placeholder such as path/to/file.py, a path git ignores (a build output or local config that was tracked once), anything inside a fenced block, and anything in a changelog produce no finding.

graph:
  enabled: true
  doc_globs: ["**/*.md"]        # which files are docs (the sources of edges)
  identifiers:
    enabled: true               # store backticked identifiers such as `parse_frame()`
    min_length: 3
    stoplist: []
  patterns: []                  # project regexes: [{name, regex, where: all|prose|code}]
  impact:
    top: 10
    min_score: 0.0
    kinds: ["mention", "cochange"]   # what docs name, and what git history shows
  scan:
    missing_paths: true
    ignore_docs: ["CHANGELOG*", "**/CHANGELOG*"]
    history_limit: 5000         # commits of history searched for deletions and renames
    dangling_symbols: false     # see "Symbol edges" below
    ambiguous_symbols: false

This section is separate from search.graph above, which only controls related: expansion inside retrieval. The top-level excluded_paths decides which docs are read; an excluded doc is never a source of edges, but any path may still be named.

Symbol edges (optional). Docs name functions and classes more often than files. With a symbol index, Carta resolves each backticked identifier to the file that defines it and stores a symbol edge (doc, defining file, the identifier, the doc line). Carta still does not parse code: it reads rows of (name, kind, file, line) that another tool wrote.

graph:
  symbols:
    provider: ctags        # none (default) | ctags | file
    file: ""               # for provider: file, a path inside the repository
    max_definitions: 1     # a name defined in more files than this makes no edge
  • provider: ctags runs universal-ctags over the repository's code files if it is installed. It is recognised by what it can do (--list-features lists json), not by its name: the ctags that macOS ships is a different program and is passed over. Set CARTA_CTAGS to name a binary. The repository's own ctags option files are not read.
  • provider: file reads a symbols file: JSON lines ({"name": "parse_frame", "kind": "function", "file": "src/frame.py", "line": 12}, optional "scope" and "language"), tab-separated name kind file line [scope], or saved ctags --output-format=json output. Any code-graph tool can be exported to the first two. The file has to be UTF-8; # lines are comments. A row counts only while its file is in the tree, so adding or deleting a code file updates the edges without the symbols file changing.
  • No provider, no binary or no file means no symbol edges, and carta graph status says which. It is never an error. An index that fails to load (too large, a symbolic link, lines but not one readable row, ctags crashing or running past its time limit) keeps the edges already stored and reports the store stale; carta impact then says that the symbol matches it printed may be out of date (symbols.stale in the JSON).

A name defined in more than one file is ambiguous. It still resolves when the doc qualifies it (Store.put, table.init) or mentions one of the defining files or a directory above it; otherwise it makes no edge. On this repository 96.5% of 3,564 indexed names are defined in a single file, and raising the limit to 2, 3 or 5 changed the replay score below by less than 0.001, so the default keeps the stricter rule. A qualifier that matches nothing in the repository makes no edge either: subprocess.check_output is not your check_output, and nothing in page.get_text says whose page it is (self., this. and cls. are the exception). Locals, parameters and import aliases are never definitions, and neither is a key in a data file (JSON, YAML, TOML, XML), a heading, or anything in a doc or under excluded_paths.

Once a provider is configured, carta impact reads symbol edges too and labels them:

 1. docs/parser.md  score 0.878890
      src/parser/frame.py  symbol: the doc names an identifier defined here (1 doc does)
        L3: Call `parse_frame()` on each chunk.

carta scan has two findings for identifiers, both off by default (graph.scan.dangling_symbols, graph.scan.ambiguous_symbols). dangling_symbol reports a doc that names an identifier which has left the code. Most identifiers no index knows are simply not yours (a library class, a shell command), so absence is never enough: the name must occur as a word in no code file today, and git must show the code had it at a commit that changed that mention in the doc. An external name fails the second test and stays silent. It needs git and no symbol index. On this repository it produced 10 findings from 103 candidates; by hand, 8 were symbols defined here and since removed, 2 were names the code once used and never defined, and 6 of the 8 sat in dated plans. That is too small a sample, and too archival, to switch on for everyone. ambiguous_symbol is informational: a name the index defines in too many files to resolve (33 here, main and __init__ among them).

What it does not do:

  • It does not parse source code. Edges come from doc text, and symbol edges from an index another tool wrote.
  • It does not read paths written in plain prose. A path must be in backticks, a markdown link, a fenced block or frontmatter. That is deliberate: "read/write" and "and/or" are not paths.
  • Backticked identifiers (parse_frame(), MAX_RETRIES) are stored, and resolved to the files that define them only when a symbol provider is configured (above). Without one they play no part in carta impact.
  • It does not follow calls. A symbol edge says where a name is defined, not who uses it.
  • It does not follow renames. A doc that still names the old path is a carta scan finding, because the text on the page is now wrong.
  • Its edges do not feed carta search.

Is it any good on your repository? carta graph replay answers that from git history with no labelling: for every mainline commit that touched both code and docs, the code it touched is the query and the docs it edited are the answer. It reports recall@k, precision@k, MRR and any-hit rate against three baselines scored on the same commits (most-edited docs, basename grep, and basename grep with the same rarity weighting). Read the population line first: it says how many commits were scanned, how many were eligible and why the rest were skipped, and a result on too few commits is labelled inconclusive rather than a pass or a fail. The report also repeats every table without "hub" docs such as a changelog. When there are hubs, judge by that second table: a method that always answers "the changelog" scores well on the first one without reading anything, so the first table mostly measures who predicts the hubs, and the verdict line is printed under the second only.

carta graph replay                          # the whole mainline history of this repository
carta graph replay --last 300 --json out.json
carta graph replay --repo ../other-repo --exclude docs/archive/    # needs no Carta config there
carta graph replay --no-config              # ignore .carta/config.yaml, use the built-in defaults

The repository being replayed is treated as untrusted: its config is never read through a symbolic link, and its graph.patterns (regular expressions) are ignored unless you pass --trust-config. The same holds for the graph arm as a whole: a doc that is a symbolic link, is not a regular file or is larger than 2 MiB is never read, the edge store refuses to open through a link, and file names are taken from git byte for byte.

Trees and file contents come from git objects only (never the working tree or the edge store), so it can be pointed at any repository, and a doc is judged on what it said at the commit's parent, not on what the commit itself added. The one thing it takes from the working tree is its settings: graph: and excluded_paths from .carta/config.yaml when that file exists. Those decide which paths are docs and therefore which commits count, so the report states where its settings came from and lists the exclusions (Settings: line, settings in the JSON).

On this repository's own history (--range origin/main --no-config, 403 commits scanned, 54 eligible) the mention arm does not beat the baselines: MRR 0.191 against 0.731 for most-edited docs and 0.226 for basename grep. Without the three hub docs only 21 commits are left, which is too few to call (0.276 against 0.564 and 0.287). The docs edited alongside code here (README, changelog) do not name files, and the docs that do name files are dated plans nobody edits again. Numbers, populations and the reading of them are in the spec.

Symbol edges can be scored the same way: --arm both=mention+symbol@max --symbols ctags. The replay may only use what a commit's parent had, so --symbols ctags re-runs universal-ctags on each parent's code, read from git objects into a scratch directory and cached per file content (the whole history above takes about 8 s). --symbols tracked:PATH reads a symbols file the repository tracks, as of each parent. --symbols file:PATH uses one file as it is today for every commit, which leaks later knowledge; it is refused unless you add --accept-symbol-leak, and the report then carries a warning. On the same 54 commits (--range 792b6690), adding symbol edges to mentions moves MRR from 0.191 to 0.201 (paired difference +0.010, 95% interval -0.000 to +0.021; 25 commits better, 4 worse, 25 tied) and recall@10 from 0.262 to 0.315; symbol edges alone score 0.147. Without the hub docs (21 commits, too few to call) the two are level, 0.273 against 0.276. So symbol edges help a little at depth and do not change the verdict. That interval sits on zero and moves with the range: ending four commits later (58 eligible) the same clean run reads +0.018 (+0.001 to +0.041), so "significant or not" is not something this history can settle. The leak is small here: today's index scores the combined arm 0.003 higher than each parent's own (paired over the 54 commits: 3 better, none worse, 51 tied).

Those numbers score the two kinds in one pool (@max). An arm with no @MODE, and carta impact itself, fuse kinds with graph.impact.combine (default norm, see co-change edges below). Under norm the same arm scores 0.185, which cannot be told from mentions alone (paired -0.006, -0.020 to +0.006). All three kinds in one arm (--arm all=mention+cochange+symbol) score MRR 0.407 against 0.489 for mentions plus co-change (paired -0.082, -0.135 to -0.031; 5 commits better, 20 worse, 29 tied); without the hub docs it is 0.355 against 0.381 (21 commits, too few to call). So on this history symbol edges do not improve the default pair of kinds. That is reported as measured; no weight was tuned to change it.

Co-change edges: what git history says moves together

Mention edges only exist where a doc names a file. A second kind of edge covers what nobody wrote down: carta graph build also walks the first-parent git history and records, for every pair of files, how often they changed in the same commit. No parsing, no model. One mainline commit is one unit (one pull request on a squash-merge repository; on a merge-commit repository the merge carries the whole branch).

$ carta graph build
Graph: 113 docs, 12,931 edges (113 re-extracted, 0 unchanged)
  history: 5,419 co-change edges from 430 commits (438 new, 3 skipped as bulk)

carta impact reads both kinds and labels every reason, so "the doc names this file" is never confused with "these changed together":

$ carta impact carta/cli.py --top 2
Top 2 of 49 docs that are tied to 1 path (113 docs, 12,991 edges)
  named: the doc names the path.   history: they changed in the same commits; that is correlation, not a claim that the doc describes it.

 1. CLAUDE.md  score 1.412548
      carta/cli.py  [history] changed together in 8 of 49 commits (git history; the doc need not name it)
      carta/cli.py  [named] exact, named by 42 docs
        L27: - argparse (stdlib) - CLI argument parsing in `carta/cli.py`

In --json a history match has "kind": "cochange". --kinds mention or --kinds cochange reads one kind alone. With --diff, a pair whose only shared commits are inside the range is left out: a change is not evidence about itself.

How it counts, and what it will not do:

  • A commit touching more than 30 paths adds nothing (max_files). A formatting sweep or a vendored drop would otherwise couple everything to everything.
  • The score is confidence: the share of the changed file's commits that also changed the other one. Two files that each change constantly, but separately, score low and get no edge (min_score 0.1). Older co-changes count for less (half_life, in commits).
  • A deleted file loses its history. A file that is moved without being edited in the same commit keeps it; a move with edits starts again.
  • It is incremental: only commits after the last one seen are read. A rebase, a branch switch or an amended commit is noticed and the history is mined again. One pass over 438 commits takes under a second.
  • A repository with no commits and a directory that is not a repository give no edges and no error. A shallow clone is mined as far as its history goes and the report says shallow: true; depth 1 gives no edges. A history git cannot read (a damaged ref, a missing object) is a failure, never an empty history: the stored edges are kept and carta graph status reports cochange_git_failed.
  • A commit over the limit is counted without its paths ever being collected, and a build with no new commit does not read the stored counts, so neither a huge commit nor a long history makes carta impact slow or large.
  • A file name is counted only if it is plain printable text: names with control characters, line separators or bidirectional overrides are never stored or printed.
  • It claims no causation, and it needs history in proportion to the size of the tree.
graph:
  impact:
    kinds: ["mention", "cochange"]
    combine: norm               # how the two rankings are fused: norm | rrf | sum | max
  cochange:
    enabled: true               # `carta graph build --no-cochange` skips it for one run
    max_files: 30               # larger commits are bulk changes and are not counted
    min_support: 1              # shared commits needed before a pair is an edge
    min_score: 0.1              # and the confidence it must reach
    measure: confidence         # confidence | lift
    half_life: 100              # commits after which a co-change counts half; 0 = never
    pending_window: 1000        # with min_support > 1: forget a pair that does not recur
    follow_renames: true        # a pure move (same content) keeps its history

Does it help? Two evals, both from git objects only.

carta graph replay --arm mention=mention --arm cochange=cochange --arm both=mention+cochange scores each kind and their union on the same commits; the co-change edges for a commit come only from commits before it. On this repository (54 eligible commits) co-change alone has MRR 0.615 against 0.191 for mentions and 0.731 for "the most-edited docs"; the union scores 0.489. Without the three hub docs 21 commits are left, too few to call (0.404, 0.276, 0.564; union 0.381). On a second, larger history (306 eligible commits, 284 without its one hub) co-change alone and the union each beat all three baselines with intervals clear of zero: MRR 0.499 and 0.526 against 0.255 for most-edited docs and 0.289 for basename grep, and 0.289 for mentions alone. The union beats co-change alone on recall@5 there (+0.039 [+0.009, +0.073]) and ties it on MRR.

carta graph holdout is the stricter test for the edges themselves: build them from the history before a cut, then for each later commit give a method one of its files and ask for the others.

carta graph holdout                                   # cut after the first 80% of commits
carta graph holdout --cut v1.4.0 --cochange lift=measure:lift --cochange base
carta graph holdout --graph imports=imports.tsv       # score another tool's file graph too

On this repository (322 commits before the cut, 63 scored after it, 397 seed queries): MRR 0.724 for co-change, 0.401 for "the most-changed files", 0.365 for "the other files in this directory". --graph NAME=PATH scores an external file-to-file graph (JSON or src<TAB>dst[<TAB>weight] lines) on the identical queries. Carta cannot check when such a graph was built, so build it from the tree at the cut.

The holdout does not score a path a commit deletes: the miner counts no co-change for a deletion, so the eval does not ask for one.

The defaults came from these runs: confidence beat lift on every population, a support floor of 2 lost recall on every population, and adding the two kinds' raw scores was no better than mentions alone, which is why they are fused after normalising each kind. That makes every number above in-sample: the defaults were chosen on the same populations the results are quoted from, and no repository or period was held back. In particular the union's recall@5 edge over co-change alone is the best of several fusion settings tried on that one population, with a lower bound of +0.009; treat it as "not worse" until it is confirmed on later history or a third repository. Numbers, populations and what is still open are in the spec.

Capturing notes (quirks, bug notes, helpful notes)

The write side of session memory. When you (or Claude) learn something durable about the project, save it:

carta remember "EZKontrol bench tests silently fail unless motor CAN is powered" \
  --type quirk --title "EZKontrol bench power" --tags can,bench

or from Claude via the carta_remember MCP tool. Notes are plain markdown files with doc_type frontmatter — quirk → docs/quirks/, bug-note/helpful-note → docs/notes/ (configurable via memory.quirks_dir / memory.notes_dir) — embedded into {project}_notes and retrieved by the same hybrid search, reranker, and proactive-recall hook as your docs. Search output labels them: [quirk] docs/quirks/2026-06-11-….md.

Notes are knowledge artifacts, not tool state: git-shareable, audited by carta scan (staleness, links), exported by carta export, and still useful if you remove Carta. Hand-written files work too — drop a markdown file in docs/quirks/ (or set doc_type: quirk in frontmatter anywhere) and carta embed routes it correctly.


Visual Embedding (ColPali/ColQwen2)

Carta supports multimodal embedding of visually-rich PDF pages (datasheets, register maps, timing diagrams) using ColPali and ColQwen2 late-interaction retrieval.

Instead of converting visual content to text (lossy), this pathway:

  1. Embeds each PDF page as 1,024 patch vectors (128-dim) directly into Qdrant's multi-vector collection
  2. Stores the raw page PNG as a sidecar payload in .carta/visual_cache/
  3. Enables visual search that returns actual page images alongside text results

Enable visual embedding:

# Install with visual dependencies
pip install 'carta-cc[visual]'

Then set in .carta/config.yaml:

embed:
  colpali_enabled: true              # force on; default is null (auto: search _visual when it exists)
  colpali_model: "vidore/colqwen2-v1.0-hf"  # HF-native variant; or vidore/colpali-v1.3-hf for lower VRAM
  colpali_device: "cpu"              # "cpu", "cuda", or "mps"
  colpali_batch_size: 1              # pages per batch (1 for CPU)
  colpali_sidecar_path: ".carta/visual_cache/"

Model Selection:

Model VRAM Speed Quality Best For
vidore/colqwen2-v1.0-hf ~8GB Slow Highest GPU servers
vidore/colpali-v1.3-hf ~6GB Medium High Balanced GPU
vidore/colSmol-500M ~3GB Medium Good CPU workstations
vidore/colSmol-256M ~2GB Fast Fair CPU-only/laptops

Notes:

  • Visual embedding is additive — existing text embedding pipeline is unchanged
  • Pages are classified automatically; only visually-rich pages are embedded
  • Visual collections are separate: {project_name}_visual (multi-vector) vs {project_name}_doc (text)
  • Search returns both text and visual results; visual hits include base64-encoded PNGs

Scoping heavy visual models

ColPali and the OCR/VLM vision pipeline are expensive. Several config knobs let you control exactly where and how they run.

colpali_scoped_paths — restrict ColPali to specific directories or globs

By default (colpali_scoped_paths: []) ColPali runs on every PDF once colpali_enabled: true. Set a non-empty list to restrict it to the directories or file patterns that actually contain visual-rich content:

embed:
  colpali_enabled: true
  colpali_scoped_paths:
    - "docs/reference/datasheets/"   # trailing slash = directory prefix
    - "docs/diagrams/**/*.pdf"       # ** glob = recursive match

Matching rules:

  • Entries ending with / match any file whose path starts with that directory prefix.
  • All other entries are glob patterns: * matches within a single path segment; ** matches across segments (including zero).
  • Empty list ([], the default) means no restriction — all PDFs receive ColPali embedding.

Files outside the configured scopes are silently skipped for ColPali; the normal text-extraction pipeline still runs on them.

colpali_device: mps — Apple Silicon acceleration

Set colpali_device: "mps" on Apple Silicon Macs to run ColPali on the GPU via Metal. Falls back to cpu automatically if MPS is unavailable.

vision_routing — OCR/VLM routing mode

Controls which model pipeline the smart router uses for non-pure-text pages:

Mode Behaviour
auto (default) Heuristic routing: STRUCTURED_TEXT→OCR, TEXT_WITH_IMAGES→VLM, FLATTENED→OCR→VLM fallback
ocr Force every non-pure-text page through OCR only; never call the VLM
vision Force every non-pure-text page through the VLM only; never call OCR
off No model calls at all — every page is treated as text-only
embed:
  vision_routing: "ocr"   # good default when glm-ocr handles your content well

vision_call_timeout_s — per-call timeout

The default is 300 seconds (raised from the old 120 s hardcoded value). Dense OCR tables can take several minutes on slower hardware; raise this if you see timeout errors:

embed:
  vision_call_timeout_s: 600   # 10 minutes for very dense pages

Recommended ColQwen successor model

vidore/colqwen2.5-v0.2 is the current recommended ColQwen successor. It loads via the same native ColQwen2 transformers path and generally outperforms colqwen2-v1.0-hf on technical documents. Swap it in without any other config changes:

embed:
  colpali_model: "vidore/colqwen2.5-v0.2"

Two-pass visual embedding

Running glm-ocr and ColPali inline during a normal carta embed adds minutes per image-heavy page and blocks the fast text-extraction pass. The two-pass workflow separates these concerns:

Pass 1 — fast text:

carta embed

Extracts text from all files as normal. Pages classified as TEXT_WITH_IMAGES or FLATTENED are recorded in the sidecar's visual_pending list instead of being processed inline. At the end of the run, Carta prints a nudge:

Visual queue: 42 page(s) across 18 file(s) await visual embedding. Run carta embed --visual to process them.

Pass 2 — slow, resumable visual processing:

carta embed --visual

Drains the visual queue. For each pending page it runs:

  1. glm-ocr text extraction → ingested into the hybrid text index
  2. ColPali page-image embedding → stored in the _visual collection

Each page is checkpointed (visual_pending → visual_done) as it completes, so the pass is resumable — interrupt at any time and re-run; only unfinished pages are retried.

Typical workflow:

carta embed              # fast text; queues image-heavy pages, prints the nudge
carta embed --visual     # slow, resumable: OCR text + ColPali for queued pages

Configuration:

embed:
  two_pass_visual: true      # default true — set false to revert to inline visual processing
  visual_timeout_s: 3600     # per-file timeout for the --visual pass (default: 3600 s)
  colpali_enabled: null      # null = auto (search _visual when it exists); true = force; false = off

Once a _visual collection has content, carta search includes it automatically (the default colpali_enabled: null is "auto") and fuses visual hits with text by rank (RRF). The readiness check runs before the ColPali model loads, so projects with no visual content pay nothing. Set colpali_enabled: false to opt out entirely.

Requirements for --visual:

The --visual pass requires the optional [visual] extra (torch + transformers for ColPali):

pip install 'carta-cc[visual]'

If the extra is absent, carta embed --visual prints install guidance and exits cleanly — the text corpus is unaffected.

Python version note: torch wheels may not be available for all Python versions. If installation fails, try a Python 3.12 venv: python3.12 -m venv .venv && source .venv/bin/activate && pip install 'carta-cc[visual]'

Scoping and memory:

Use colpali_scoped_paths (see above) to restrict which directories receive visual treatment. To avoid memory pressure from glm-ocr and ColPali loading simultaneously, set the Ollama concurrency limit before running the visual pass:

export OLLAMA_MAX_LOADED_MODELS=1
carta embed --visual

Corpus integrity

carta doctor includes a corpus-integrity section that checks your embedded knowledge base for data-quality issues — no Ollama calls required:

  • Slug collisions — multiple files with the same filename stem that would have silently overwritten each other's Qdrant points (fixed in the embed pipeline; doctor flags any legacy cases).
  • Empty-text points — points stored with an embedding-of-empty-string (common after PDF extraction failures) that are unfindable in practice.
  • Chunk-count mismatches — sidecar says N chunks but Qdrant holds a different count, indicating a partial embed.
  • Stuck-stale sidecars — files whose sidecar never transitioned back to embedded after a successful re-embed.

Findings are included in carta doctor's JSON output alongside the existing environment checks.

Repairing a corpus:

carta embed --repair

Purges and force re-embeds all files flagged by the integrity checks, and fixes stuck-stale sidecars in place. The summary reports: repaired / purged / flagged extraction_failed / queued-for-visual / failed.

Agents: see docs/field-notes.md for hard-won operating notes — running an unreleased build against another repo, the supersession-judge latency / judge_errors gotcha, the eval-corpus validation loop, and the doctor → embed --repair flow.


Status-line progress widget

While carta embed runs, it writes .carta/embed-status.json with live progress. The carta statusline command reads that file and prints a compact segment for the Claude Code status line:

⠹ carta 24/47  big.pdf  19m
✓ carta 47 files · 3.2k chunks
✗ carta 24/47 · 2 errors

Auto-wiring during carta init

If your ~/.claude/settings.json has a statusLine.command pointing to a .sh file, carta init will offer to wire the segment in automatically. It writes a .bak backup before editing.

Manual snippet — add this to your status-line script, before the line that prints $parts:

seg=$(command -v carta >/dev/null && carta statusline <<<"$input" 2>/dev/null)
[ -n "$seg" ] && parts="$parts │ $seg"

CLI wiring

carta statusline --install    # wire into the script found in settings.json
carta statusline --uninstall  # remove the wired block

.carta/embed-status.json is regenerated each run and should be gitignored (Carta does this automatically).


Sharing an embedded project

Embedding is the expensive part — vision models run over every PDF page and ColPali produces multi-vector visual embeddings, which can take hours on a capable GPU. A collaborator working on the same repository doesn't need to repeat that. Once one machine has embedded the project, it can hand the embedded state to another in one command on each side.

On the machine that has embedded the project:

carta export                       # -> ./carta-<project>-<date>.tar.gz
carta export -o ~/share/carta.tar.gz
carta export --no-visual           # skip the _visual collection (smaller bundle)

This snapshots the project's Qdrant collections (_doc, _notes, _session, and _visual unless --no-visual) and packs them with a copy of config.yaml, the sidecars/ metadata, and a manifest into a single .tar.gz. The visual collection is included by default.

On the receiving machine (Qdrant running, same qdrant/qdrant version):

carta import carta-myproject-20260608.tar.gz
carta import bundle.tar.gz --project otherproject   # restore under a different name
carta import bundle.tar.gz --force                  # overwrite existing collections

Import restores each collection, writes config.yaml and any missing sidecars into .carta/, and then carta search works immediately — no re-embedding. Existing sidecars are never overwritten; existing collections block the import unless --force is passed.

Notes

  • Querying still needs Ollama (nomic-embed-text) to embed the search query — lightweight, runs on CPU. Visual search additionally loads ColPali/ColQwen2 at query time, so set colpali_device: cuda on a machine with an NVIDIA GPU.
  • Snapshots are Qdrant-version-coupled. Run a matching qdrant/qdrant version on both sides; import warns (but proceeds) on a version mismatch.
  • Keep your repo, Qdrant storage, and Ollama models on a native Linux filesystem rather than a slow mount (e.g. under WSL, use /home/..., not /mnt/c).

Issue lifecycle

Carta assigns stable AUDIT-NNN IDs that survive across audit runs:

new → persisting → needs-input → resolved → archived

After needs_input_at_audit_count consecutive audits without resolution, an issue is escalated to needs-input and added to docs/BACKLOG/TRIAGE.md as a DOC-NNN item. The audit report is the single source of truth — no separate state file.


What Carta doesn't do

  • It doesn't replace your wiki or CMS.
  • It doesn't auto-fix contradictions (it surfaces them; you or your agent decides what to do).
  • It doesn't require a cloud service — everything runs locally by default.
  • It doesn't add much overhead to projects with simple, stable docs.

Contributing

Issues and PRs welcome. The scanner, embed pipeline, and skill files are all designed to be readable and hackable.


License

MIT. Carta depends on PyMuPDF, which is licensed separately under AGPL-3.0 (or a commercial licence from Artifex); if you distribute software built on Carta, its terms apply to that dependency.

Metadata

Release files for carta-cc 0.18.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for carta-cc 0.18.0
File Size Uploaded
carta_cc-0.18.0.tar.gz 729.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for carta-cc 0.18.0
File Interpreter ABI Platform
carta_cc-0.18.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.5 MB

Release files / carta_cc-0.18.0.tar.gz

Download URL carta_cc-0.18.0.tar.gz
Size 729.3 kB
Tags Source
SHA-256 checksum
How to use checksums
cdf91c5d811cf6fb4a639c9687d1749e630f5c80e52b3d243125d6bc522e3bbb
BLAKE2b-256 checksum
How to use checksums
4eb55a9c800f0f7e3c90687f3e923f937bdb73d7988ab23906ed7fd9cd189175
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.17

Release files / carta_cc-0.18.0-py3-none-any.whl

Download URL carta_cc-0.18.0-py3-none-any.whl
Size 779.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c1af7609e6ce6fba846d9392c258a7a56df94f368cfb73b9473847a7574ccfe8
BLAKE2b-256 checksum
How to use checksums
0070719b31fac75deeff8eacc4c0f40b1de99dbff7b96e958ba0cf8f8afbd735
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.17

Release history Release notifications | RSS feed

This release

0.18.0 This release

2 release files

0.16.1

2 release files

0.16.0

2 release files

0.15.0

2 release files

0.14.0

2 release files

0.13.0

2 release files

0.12.4

2 release files

0.12.3

2 release files

0.12.2

2 release files

0.12.1

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.7

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.3.15

2 release files

0.3.9

2 release files

0.3.8

2 release files

0.3.7

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page