Skip to content

Latest commit

 

History

History
281 lines (207 loc) · 13.7 KB

File metadata and controls

281 lines (207 loc) · 13.7 KB

Carts — how a knowledge cartridge is created

A cart (cartridge) is Aporia's domain coprocessor: a self-contained bundle of parsed structure and embeddings over ONE body of source code, plus the checkers that verify claims against it. A host model (any LLM) talks to a cart only through a narrow contract, so the cart is portable across hosts. This document explains what a cart is, the end-to-end pipeline that builds one, how to point the builder at your own source tree, and why no cart DATA ships in this repo.

Cart data is absent from this repo by policy. You build carts from your OWN sources. See Why cart data is not in the repo below.


What a cart is

A cart has three parts, all derived from one source tree:

  1. A parsed API indexclass -> {methods: {name: (return_type, specifiers, is_const)}, delegates: {name: arity}}. Built by parsing headers (build_index in checker.py, parse_header in generate_claims.py). Pure structure, no model, zero variance. Since 2026-07-19 this also includes a call-graph index (who calls whom, who Broadcasts what, from brace-matched .cpp bodies), and all parse products are served through a content-keyed cache (index_cache.py) — rebuilds cost the diff, not the corpus.
  2. An embedding index — every source chunk embedded to a 768-d vector via a local embedding model, cached on disk. This is the semantic-retrieval half, used to pull relevant code as evidence for prose claims.
  3. The checker pipeline over both — a fast deterministic checker (index lookups) with an LLM-judge fallback for prose/behaviour claims (checker.py, runtime_verify.py). Each claim resolves to flag (source contradicts it), pass (source confirms it), or abstain (cannot be decided from source — holding aporia).

The cart implements the Slot Contract (cartridge_contract.py): the host passes raw domain text in and gets cartridge-native scalars and vectors out. No host hidden-states, tokenizer, or embeddings ever cross the boundary — that single rule is what makes one cart usable by any host model.


The creation pipeline, end to end

The pipeline turns a source tree into a working, verifiable cart. Each stage has code that owns it and a done-criterion you can check before spending time on the next stage.

1. Point the builder at a source tree

The active source roots come from domain_roots() in corpus.py, selected by environment variable:

# default domain
.venv\Scripts\python.exe corpus.py

# switch to the secondary (large) domain sample
$env:CARTRIDGE_DOMAIN = "ue"

Two environment knobs shape what gets ingested (both read in corpus.py):

  • CARTRIDGE_FILTER — keep only headers whose text contains this token (e.g. an API-export macro). This is how a huge tree is narrowed to API-rich headers instead of everything.
  • CARTRIDGE_MAXFILES — an optional global cap on file count, for quick smoke runs.

The ingester skips generated and build-output directories and auto-generated header suffixes (_SKIP_DIRS, _SKIP_SUFFIXES in corpus.py), so build artifacts never pollute the cart.

Done-criterion: corpus.py prints file and chunk counts for your roots. Non-zero, and the numbers look plausible for your tree.

2. Chunk the corpus

chunk_text() splits each file into fixed line-windows (default 12 lines, minimum 40 characters). Fixed windows are deliberate — no language parser, so the same chunker works on any text. file_chunk_lists() returns per-file chunk lists; a sliding window of consecutive chunks within one file is the predictive unit ("reconstruct a masked chunk from its neighbours in the same file").

Done-criterion: chunk counts are stable and reproducible across runs on the same tree.

3. Build the parsed API index

build_index() (checker.py) walks the headers and calls parse_header() (generate_claims.py) to extract, per class:

  • methods — return type, blueprint/authority specifiers, and const-ness, read off each exported-function declaration;
  • delegates — multicast-delegate declarations and their parameter arity.

This index is the ground truth the deterministic checker looks claims up against — no model, no cost, no variance.

Done-criterion: the index reports a plausible class and method count for your tree, and the deterministic eval (below) holds ~100% on structural claim types before you spend time embedding. warm_ue_cache.py runs this as an explicit gate: if the structural real-pass or fake-catch rate drops below 99%, it stops and tells you to investigate the parser on your headers rather than proceeding to the expensive embed stage.

4. Embed and cache the chunks

embed.py is the cart's input encoder: raw text in, 768-d vector out, via a local embedding model served by Ollama. Vectors are cached on disk (a keys file plus a binary vectors array) so the cart never cold-embeds at startup:

  • batched requests with periodic incremental saves (a crash loses at most one save window);
  • an oversized/poison chunk is isolated by bisecting the batch and given a zero vector rather than killing an hours-long run;
  • a one-time migration path folds any legacy single-file cache into the binary format.

For a large tree, pre-warm the cache with a dedicated one-shot run (see warm_ue_cache.py, which counts the corpus, runs the deterministic gate, then embeds the full filtered corpus in large batches). After warming, the registered server starts fast because every vector is already on disk.

Done-criterion: the embed cache exists and its vector count matches the chunk count from stage 2. A second run reports everything cached (no new embeddings).

5. Wire the checker pipeline

The cart exposes verification through two checkers run in order (checker.py); the first non-abstain verdict wins:

  1. Deterministic checker — resolves structural claims (return type, method existence, authority/purity specifiers, delegate arity) as pure index lookups. Abstains on anything it cannot reduce to a parsed fact — which is exactly the hand-off point to the judge.
  2. Judge checker — the fallback for prose/behaviour claims. It retrieves the relevant real code (symbol-guided plus embedding-nearest), attaches the exact doc-comment and implementation body as evidence, and asks the local judge model for one word: contradicted / consistent / unsure. Unsure maps to abstain — the judge never bluffs.

The runtime loop (runtime_verify.py) is the actual-use path: it first asks the extractor model to convert a prose statement into typed facts (EXISTS, RETURN_TYPE, AUTHORITY, PURITY, DELEGATE_ARITY, CALLS, and BEHAVIOR for everything else), each carrying a Class::Member anchor. Structural facts are then checked as index lookups; CALLS facts ("X calls Y" / "X fires OnZ") resolve against the call-graph index (direct = pass, one hop = pass at reduced confidence, absent = flag); BEHAVIOR facts route to the judge carrying their anchor, so symbol-precise evidence always attaches. Every anchor also gets an implicit existence check, so an invented method is caught no matter which fact type the extractor chose.

Every verdict carries confidence + basis (discrete rubric in checker.py): extracted 1.0 for a parsed-index fact, inferred 0.55–0.9 graded by evidence tier, and ambiguous 0.0 for every abstain — the labeled null, never a fake answer.

Done-criterion: a round-trip smoke test passes — a true statement about a real indexed method returns pass, a wrong-return-type variant returns flag, and an invented-method statement returns flag. build_ue_cart.py runs exactly this smoke round-trip after building the index and embeddings.

6. Serve the cart

verify_server.py exposes the runtime loop as a local MCP tool (verify_statement) so any host can call it inline. The heavy state (parsed index, embeddings, judge corpus) loads once at server start; individual calls are then fast. See REPRODUCE.md for registration shape and environment.

7. Keep it fresh (a cart must never silently serve a stale index)

If the indexed sources are git clones, run install_hooks.py once: it drops post-commit / post-checkout hooks that touch a dirty marker and spawn a detached prewarm_cache.py. The server stats the marker on every call and hot-refreshes when it's newer than the index — a commit lands in the verifier without a restart, and the content-keyed cache makes the refresh cost the diff. For non-git sources (e.g. an installed engine tree), both servers expose a refresh_index MCP tool; describe reports index_built so staleness is always visible.

Done-criterion: freshness_calls_test.py passes (cache serves unchanged files without re-parse; an edit lands after refresh without restart), and a commit in a hooked clone is verifiable on the next verify_statement call.


Pointing the builder at YOUR OWN source tree

The whole pipeline is domain-agnostic. To build a cart over your own code:

  1. Define a source-roots list. In corpus.py, add a list of directory Paths for your tree and return it from domain_roots() (mirror how the existing roots are selected by the CARTRIDGE_DOMAIN environment variable). Point it at the directories that hold the source you want the cart to know.

  2. Set the file extensions. The C/C++ builders use (".h", ".cpp"); a Python cart uses (".py",). Pass the extension tuple your language uses to iter_files / file_chunk_lists.

  3. Tune the header parser to your API surface (only if you want structural checks). The deterministic checker's power comes from parsing your declaration syntax. parse_header() is written for one C++ export/annotation style — the class-declaration regex, the function-declaration regex, the specifier keywords, and the delegate macro. If your codebase annotates its API differently, adapt those regexes to match. If you skip this, the deterministic checker simply abstains more and the judge does more of the work — the cart still functions, just with less zero-cost coverage.

  4. Set the content filter for large trees. If your tree is big, choose an CARTRIDGE_FILTER token that marks the API-rich files worth indexing (an export macro, an annotation), so you index the interface rather than every file.

  5. Warm the cache and run the gate, then register the server (stages 4–6 above).

Nothing else in the pipeline is domain-specific: chunking, embedding, retrieval, the judge prompt shape, and the trinary contract are all generic.


Sanitized example fact strings

These are synthetic illustrations of the claim/verdict shapes the cart handles. The class, method, type, and delegate names are invented for documentation — they are not from any real codebase. Substitute your own symbols.

Structural — return type (deterministic checker):

CLAIM:  UWidgetCatalog::GetEntryCount returns int32.
        -> PASS   (deterministic: index says GetEntryCount returns int32)

CLAIM:  UWidgetCatalog::GetEntryCount returns bool.
        -> FLAG   (deterministic: claims bool, actually int32)

Structural — existence:

CLAIM:  UWidgetCatalog exposes a RebuildEntries() method.
        -> PASS   (deterministic: RebuildEntries exists in source)

CLAIM:  UWidgetCatalog exposes a TotallyInventedMethodXYZ() method.
        -> FLAG   (deterministic: no method named TotallyInventedMethodXYZ() anywhere in source)

Structural — delegate arity:

CLAIM:  UWidgetCatalog's OnEntriesRebuilt is a two-parameter delegate.
        -> PASS   (deterministic: OnEntriesRebuilt takes two params)

CLAIM:  UWidgetCatalog's OnEntriesRebuilt is a three-parameter delegate.
        -> FLAG   (deterministic: claims three, actually two params)

Behavioural — routed to the judge (typed as BEHAVIOR):

CLAIM:  UWidgetCatalog::RebuildEntries fires the OnEntriesRebuilt event.
        -> PASS   (judge: implementation broadcasts OnEntriesRebuilt — consistent)

CLAIM:  UWidgetCatalog::RebuildEntries fires the OnCatalogCleared event.
        -> FLAG   (judge: implementation broadcasts OnEntriesRebuilt, not OnCatalogCleared)

Inherently null — held as aporia (never flagged):

CLAIM:  UWidgetCatalog lives on the player pawn.
        -> ABSTAIN (ownership/location is a design decision — a spawnable component can live on
                    any actor; not verifiable from source)

A prose statement typically decomposes into several typed facts, each verified independently; the runtime loop reports per-fact verdicts and a summary count (flagged / unverifiable / confirmed).


Why cart data is not in the repo

The cart BUILDER code is tracked in this repo — it is the author's own code, and it is what you reproduce from. But no cart DATA output ships, by policy:

  • The embedding caches ARE the cart data. They are embeddings of the source that was indexed. Shipping them would ship a derivative of that source.
  • The secondary-domain cart derives from a commercial engine's licensed source, whose license does not permit redistributing derivatives — so its embeddings and any file carrying its symbols are excluded.
  • The primary cart derives from the author's unreleased code, which is not published.
  • Downstream artifacts carry those symbols too (the field log, the learned-fact store), so they are excluded on the same grounds.

The exclusion is enforced in .gitignore (the embed caches, *.npy, the field logs, and the learned-fact store are all listed). This is intentional and load-bearing: the repo is a reproducible blueprint, not a data drop. You build carts from your own sources using the builder code here, and the sanitized examples above show the shapes to expect.