# Embedding arborist in another Python app

arborist is usable as a library: another Python project can use it as a
content-addressed, Merkle-committed, audit-chained store for documents it
already has — without the CLI, a server, or any wire protocol. The
supported surface is **`arborist.embed`**. Import from there, not from
arborist's internal modules, so internal refactors don't break you.

This is how a downstream archival/crawler app (e.g. neopig) can gain
verifiable dedup, FTS5 search, and an append-only audit chain over its
archived page text while keeping its own storage for everything else.

## The contract

1. **You produce `Document`s.** A `Document` is a `uri` + normalized text
   `content` + a `source_type` tag (+ optional `title`, `edges`, `extra`).
2. **You hand them to ingest.** arborist canonicalizes → chunks → Merkle-
   roots → FTS5-indexes → writes one audit event. Idempotent: same content
   → same `document_root` → no-op.
3. **arborist owns its SQLite file.** Every write goes through this API;
   you never touch arborist's tables. (That also means an ORM app with a
   "no raw SQL" rule stays clean — there's no SQL for you to write.)
4. **Your own content-address key is orthogonal.** If you already dedup by
   md5 (or anything), keep it — arborist's `document_root` is an
   *additional* hard-hash commitment, not a replacement. See the
   soft-hash vs hard-hash rule in the project CLAUDE.md.

`arborist.embed` is **core** (python + sqlite3); no `[crawler]`/`[nli]`/
`[vec]` extra is needed to embed.

## Minimal use

```python
from arborist.embed import open_store, ingest_documents, search, Document, Edge

conn = open_store("data/arborist.db")          # creates + migrates schema

ingest_documents(conn, [
    Document(
        uri="https://example.com/post",
        content="the page's extracted prose",
        source_type="neopig_html",
        title="A Post",
        edges=[Edge(edge_type="embeds_media", dst_uri="https://example.com/img.jpg")],
        extra={"crawl_job_id": 7, "md5": "..."},   # your provenance, carried along
    ),
])

for hit in search(conn, "extracted prose", limit=10):
    print(hit.document_uri)

conn.close()
```

For a stateful corpus, subclass `Source` (set `source_type`, implement
`iter_documents()`) and call `arborist.ingest.ingest_source` — exactly how
arborist's own `sources/` are written. For full multi-route retrieval /
RAG, use `arborist.qa.query` directly.

## What you get

- **Content dedup with proofs** — identical content (across different URIs)
  collapses to one `document_root`; per-chunk leaves expose partial
  overlap. See `docs/crawler.md`.
- **FTS5 search** over chunked content.
- **Append-only audit chain** — every ingest is one hash-linked
  `audit_events` row; tamper becomes detectable.
- **Mesh-ready** — peers can re-derive and cross-verify roots.

## Surface (`arborist.embed`)

| Symbol | What |
|---|---|
| `open_store(db_path)` | open/create/migrate a store; returns a `sqlite3.Connection` you own |
| `ingest_documents(conn, docs, *, source_type=…, **kw)` | ingest an iterable of `Document`s; returns `IngestStats` |
| `search(conn, query, *, limit=20)` | FTS5 BM25 over chunk content; returns `list[Hit]` |
| `Document`, `Edge`, `Source`, `Hit`, `IngestStats` | re-exported types |
