Metadata-Version: 2.4
Name: carta-cc
Version: 0.18.0
Summary: Semantic memory sidecar for Claude Code projects: doc audits, a local vector knowledge base, and proactive recall
Author: Ian-q
License: MIT
Project-URL: Homepage, https://github.com/Ian-q/Carta
Project-URL: Issues, https://github.com/Ian-q/Carta/issues
Project-URL: Changelog, https://github.com/Ian-q/Carta/blob/main/CHANGELOG.md
Keywords: claude-code,documentation,rag,semantic-search,qdrant,ollama,mcp
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Documentation
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyYAML>=6.0
Requires-Dist: qdrant-client>=1.7
Requires-Dist: requests>=2.31
Requires-Dist: pymupdf>=1.23
Requires-Dist: mcp<2,>=1.7.1
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: openpyxl>=3.1; extra == "dev"
Provides-Extra: spreadsheet
Requires-Dist: openpyxl>=3.1; extra == "spreadsheet"
Provides-Extra: hybrid
Requires-Dist: fastembed>=0.4; extra == "hybrid"
Provides-Extra: visual
Requires-Dist: torch>=2.0.0; extra == "visual"
Requires-Dist: transformers>=4.49.0; extra == "visual"
Requires-Dist: accelerate>=0.26.0; extra == "visual"
Requires-Dist: pillow>=10.0.0; extra == "visual"
Requires-Dist: numpy>=1.24.0; extra == "visual"
Dynamic: license-file

![Carta](https://raw.githubusercontent.com/Ian-q/Carta/main/docs/assets/banner.png)

[![Test](https://github.com/Ian-q/Carta/actions/workflows/test.yml/badge.svg)](https://github.com/Ian-q/Carta/actions/workflows/test.yml)
[![PyPI](https://img.shields.io/pypi/v/carta-cc)](https://pypi.org/project/carta-cc/)
[![Python](https://img.shields.io/pypi/pyversions/carta-cc)](https://pypi.org/project/carta-cc/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)

# 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](https://qdrant.tech) vector store via [Ollama](https://ollama.ai). Generates `spec_summary` blocks for dense documents so the audit agent can cross-reference them without re-reading 200 pages.

### 3. Search

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](https://huggingface.co/spaces/vidore/vidore-leaderboard) v1/v2** (nDCG@5) — the visual-document-retrieval benchmark ColPali/ColQwen2 are evaluated on; the most direct check of Carta's visual layer.
- **[BEIR](https://github.com/beir-cellar/beir)** / **[MTEB retrieval](https://huggingface.co/spaces/mteb/leaderboard)** / **[RTEB](https://huggingface.co/blog/rteb)** (nDCG@10) — standard text-retrieval generalization.
- **[FreshStack](https://fresh-stack.github.io/)** — 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](CHANGELOG.md). **Install (pipx, venv, PATH):** [docs/install.md](docs/install.md).

### Claude Code plugin (recommended)

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

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

```bash
# 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](docs/install.md)** for pipx vs venv, PATH, PlatformIO conflicts, and `--pip-args` syntax.

---

## Setup (5 minutes)

**Prerequisites:**

```bash
# 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](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:

```yaml
---
related:
  - CLAUDE.md
  - docs/api/endpoints.md
last_reviewed: 2026-03-20
---
```

3. Run your first audit: `/doc-audit` in Claude Code (or `carta scan`)
4. Embed your reference PDFs: `/doc-embed` (drop files into `docs/reference/`)
5. 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:

```yaml
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:

```yaml
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:

```bash
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):

```bash
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](docs/superpowers/specs/2026-09-21-hyde-hypothetical-query-design.md).

### 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**:

```yaml
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.

```bash
carta graph build      # incremental by content hash; --force re-extracts everything
carta graph status     # counts, digest, and whether the store is stale (never builds)
```

```text
$ 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):

```bash
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
```

```text
$ 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.

```yaml
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.

```yaml
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](https://ctags.io) 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:

```text
 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.

```bash
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](docs/superpowers/specs/2026-10-09-graph-arm-edge-store-design.md#results).

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

```console
$ 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":

```console
$ 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.

```yaml
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.

```bash
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](docs/superpowers/specs/2026-10-09-graph-arm-edge-store-design.md#co-change-edges-130).

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

```bash
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](https://github.com/illuin-tech/colpali) and [ColQwen2](https://huggingface.co/vidore/colqwen2-v1.0) 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:**

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

Then set in `.carta/config.yaml`:

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

```yaml
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 |

```yaml
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:

```yaml
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:

```yaml
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:**

```bash
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:**

```bash
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:**

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

**Configuration:**

```yaml
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):

```bash
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:

```bash
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:**

```bash
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`](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`:

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

**CLI wiring**

```bash
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:**

```bash
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):

```bash
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](LICENSE). Carta depends on [PyMuPDF](https://pymupdf.readthedocs.io), 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.
