@ornyx/cartographer
v0.1.0
Published
Deterministic repository inventory and structural graph engine.
Maintainers
Readme
Cartographer
Cartographer builds a deterministic, durable, queryable map of a repository — inventory, structure, and the evidence behind every fact — so that a coding agent can look something up instead of guessing.
It is a library first: five separately callable layers plus a lifecycle facade, with a JSON-first CLI and an MCP server over the same API. It does static analysis only; it never executes, imports, or evaluates the code it reads.
This repository starts from the working Cartographer prototype originally developed inside Rector. It is now an independent Apache-2.0 project and a foundation for further development.
Why this one is different
Four properties are unusual, and each one is checked rather than asserted.
1. Deterministic, byte-identical structural output. Counts, snapshot ids, node and edge ids, paged SHA-256 hashes over canonical records, and operation result fingerprints reproduce exactly. Two consecutive full benchmark runs produced a structural.json with the same SHA-256, and re-running one corpus from a different absolute directory produced a byte-identical structural record — so absolute paths do not leak into ids (BENCHMARKS.md § Determinism; npm run bench:public -- --require-stable fails on drift). Timings are not deterministic and are never presented as if they were. Byte-identity across different machines or different Node.js versions is expected but has not been demonstrated, so it is not claimed.
2. Stable, content-addressed IDs. ID policy v3 scopes ids to a logical repository identity rather than a checkout path, and symbol ids are line- and reorder-independent (lexical scope plus declaration signature). Moving a function down a file does not change its id, and two clones of the same repository at different paths agree (tests/cartographer/graphIds.test.ts, docs/architecture.md).
3. A typed error taxonomy, not string matching. Every failure mode is a distinct exported class carrying structured fields — which limit was exceeded and by how much, which SQLite condition occurred and whether it is retryable, which input was rejected. See Errors an agent can branch on.
4. Evidence with explicit confidence, and honest uncertainty. Every emitted edge and derived node carries a canonical evidenceRecord with a qualitative confidence class, a method, the adapter and schema versions that produced it, and the file hashes it depends on (docs/API.md § Evidence and confidence). There are no numeric relevance or confidence scores anywhere. Where a fact cannot be proven, Cartographer says so: an unresolved import stays recorded on the importing file and never becomes an edge to an invented target; truncated results are flagged as truncated; counts that are lower bounds are labelled lower bounds; and verifyClaims returns not-derivable rather than guessing.
Static analysis only. Cartographer reads and parses; it does not run your code, your build, your tests, or your package scripts. The single subprocess in the codebase is icacls on Windows, used to lock down permissions on Cartographer's own state directory (src/cartographer/filePermissions.ts).
Requirements
- Node.js 22.13 or newer (
node:sqliteis required, and it is only available unflagged from v22.13.0 — on 22.5.0–22.12.x it needs--experimental-sqlite) - npm 10 or newer
- ESM-only package consumption: use
importsyntax. No CommonJS entry is exported;require()via package exports is unsupported. Node.js>=22.13.0remains required.
Install
The package is not published yet. For local development:
npm install
npm run verifyQuick start
Index once, then read. Snapshot ids are optional on every read — they resolve to what index() just wrote.
import { SEARCH_SCHEMA_VERSION, initializeRepository } from "@ornyx/cartographer";
const repository = await initializeRepository({ repoRoot: process.cwd() });
try {
await repository.index();
// Where is this thing?
const hits = await repository.search({
schemaVersion: SEARCH_SCHEMA_VERSION,
query: "compileTaskContext",
});
// Give me a bounded, budgeted briefing for a task.
const context = await repository.context({
task: "Fix the task-context byte budget",
objective: "change",
budget: { maxBytes: 16_384 },
});
// What else does this change touch?
const blast = await repository.impact({
changedNormalizedPaths: ["src/cartographer/taskContextCompiler.ts"],
});
console.log(
hits.hits.length,
context.primaryNodes.length,
blast.classifications.length,
);
} finally {
await repository.close();
}initializeRepository is idempotent and creates the database at .cartographer/cartographer.db by default. openRepository reopens an existing one; indexRepository is the initialize/index/close one-shot for scripts. update() is incremental and returns status: "fresh" as a true no-op when nothing changed. Full lifecycle, options, cancellation, and limits: docs/API.md.
Snapshot resolution
Omitting snapshotId resolves, in order, to the snapshot this handle last wrote, then to the store's latest snapshot header (greatest createdAt, ties broken by smallest id — a fixed total order over persisted rows, with no clock read at call time), and otherwise throws RepositoryNotInitializedError rather than returning an empty result. An explicitly supplied id is never silently replaced by the latest one. diff keeps beforeSnapshotId mandatory, because defaulting both ends of a comparison has no unambiguous meaning. Full rules: docs/API.md § Snapshot resolution.
Checking what a model claims about the repository
verifyClaims takes structured claims — the kind an LLM emits while reasoning about a codebase — and returns a deterministic three-valued judgment per claim, with graph evidence and a templated (never synthesized) explanation.
const result = await repository.verifyClaims({
schemaVersion: CLAIM_VERIFICATION_SCHEMA_VERSION,
claims: [
{ kind: "file-exists", path: "src/cartographer/repository.ts" },
{
kind: "symbol-exported-from-module",
modulePath: "src/cartographer/repository.ts",
symbol: "initializeRepository",
},
{
kind: "symbol-calls",
callerSymbol: "index",
callerPath: "src/cartographer/repository.ts",
calleeSymbol: "buildGraphSnapshot",
},
],
});
for (const judgment of result.judgments) {
console.log(judgment.verdict, judgment.reason);
}Read the caveat before you rely on this. The verdicts are supported, contradicted, and not-derivable, and the value of the API is the third one. contradicted is returned only where the graph's absence of a fact is documented proof that the fact is false — file and directory existence always, symbol and export claims only for files classified as source, import claims only when the importer has no unresolved imports. Everywhere else the answer is not-derivable with a machine-readable reason from a closed set. In particular, symbol-calls never returns contradicted: call-edge coverage is partial in syntax mode and still does not model runtime dispatch in semantic mode, so a missing call edge is not proof of a missing call.
So this is not a hallucination detector, and treating not-derivable as a soft "no" would reintroduce exactly the error it exists to prevent. It is a checker that tells you precisely when it can settle a question and when it cannot. The full absence-provability table is the contract: docs/API.md § Claim verification, tested in tests/cartographer/claimVerifier.test.ts.
CLI
cartographer is a JSON-first wrapper over the same public API, for agents that do not run Node in-process.
cartographer index
cartographer search --query "initializeRepository" --match-class symbol --max-results 20
cartographer impact --path src/cartographer/graphBuilder.tsstdout is exactly one JSON document per invocation and nothing else; stderr carries diagnostics only (--quiet silences it). Exit 0 is success, 1 is a typed Cartographer error, 2 is a usage error — both error codes print the typed error name and its structured fields as JSON. Read commands are byte-reproducible for the same repository state; index, update, and status route their non-deterministic timings block to stderr so stdout stays stable. Full reference: docs/CLI.md.
MCP server
packages/cartographer-mcp exposes seven tools over the Model Context Protocol — sync, status, search, context, impact, diff, and verify_claims. Every read reports a freshness state (fresh, stale, unknown, or not-indexed), so a client is told when the index is behind the working tree instead of silently receiving stale answers.
Errors an agent can branch on
Failures are exported classes with structured fields, so recovery does not depend on parsing messages.
| Error | Fires when |
| --- | --- |
| RepositoryNotInitializedError | Opening a repository with no database, or reading a snapshot that does not exist or belongs to another repository |
| CartographerRepositoryClosedError | Any indexing operation after close() |
| LimitExceededError | A configured maxFiles, maxTotalBytes, maxGraphNodes, or maxGraphEdges budget is exceeded; carries the limit, the actual and maximum values, and diagnostics so far |
| CartographerAbortedError | A supplied AbortSignal fires; transactions are never interrupted mid-commit |
| CartographerPathEscapeError | A resolved path leaves the repository root; carries the offending absolute path. The scanner never reads outside the root |
| UnsupportedSchemaVersionError | The database was written by a newer schema version |
| CartographerStoreSqliteError | An underlying SQLite failure; carries a classification and a retryable flag. CartographerStoreContentionError and CartographerStoreConstraintError are the specific subclasses |
| CartographerStoreCorruptionError | A persisted row cannot be decoded as a valid Cartographer record |
| CartographerStoreNotSupportedError | A store does not implement a capability an operation requires — third-party stores fail this gate honestly rather than degrading silently |
| GraphStoreLimitExceededError | A traversal request exceeds the exported hard depth/node/edge caps |
| TaskContextBudgetInsufficientError | The irreducible evidence floor cannot fit the requested byte budget; carries both budgetBytes and floorBytes instead of returning an empty or over-budget envelope |
| CartographerTaskContextInputError / CartographerClaimInputError | Caller input was rejected by validation; carries the offending hint or the failing issues |
| CartographerInternalInvariantError | An engine, store, or adapter contract was violated — this one is a bug report, not a usage error |
Performance
Measured, reproducible, and published with the unflattering results included: BENCHMARKS.md.
Cold index — walk, ignore, hash, classify, parse, build the graph, and persist to SQLite — ran at 284.7–408.5 files/s on three pinned public repositories of 120, 228, and 233 files (single-threaded, syntax-only adapters, median of 3 process-isolated runs, on the machine described on that page). Persistence alone is 39–45% of that time, which is the price of producing a durable, reopenable, integrity-checkable artifact rather than an in-memory index.
Two things that page is careful about, and so is this one. Those are small repositories and the throughput does not hold at scale: archived measurements on 4 700–6 100-file repositories imply roughly 117–184 files/s. And incremental update() currently saves only 4–13% over a cold index for a one-file edit, because every update mints a new snapshot and persists Θ(N) rows; that is a known storage-format problem and it is not fixed in 0.1.0.
Using Cartographer from an agent
docs/agents.md is the playbook: which read operation answers which question, how to detect a stale index before trusting a result, how to spend a token budget, which parts of the JSON are stable enough to cache, and how to use verifyClaims without over-reading it.
Primitives
The lifecycle facade is optional. Every layer is separately callable.
import { scanRepository } from "@ornyx/cartographer";
const result = await scanRepository({ repoRoot: process.cwd() });
console.log(result.snapshot, result.files.length, result.errors.length);- Inventory —
scanRepository,scanChangedFiles,classifyFile,hashFile, ignore-policy helpers. Walks without following symlinks, applies nested.gitignoreand the root legacy.rectorignore, and excludes.cartographer/. - Graph —
buildGraphSnapshot,buildIncrementalGraphSnapshot,analyzeTypeScriptSemantics, the language-adapter registry. Project, package, directory, file, symbol, tool, capability, and eval records; TypeScript/JavaScript and Python syntax extraction, plus an optional compiler-backed semantic layer for typed calls, references, inheritance, and path aliases. - Persistence —
InMemoryCartographerInventoryStore/SqliteCartographerInventoryStoreand the matching graph stores, behind identical interfaces. - Comparison —
compareGraphSnapshotsFromStore(store-native, paged) and the legacy hydratedcompareGraphSnapshots. Move/rename matching is conservative; ambiguous candidates stay unmatched and are reported as ambiguous. - Read paths —
searchRepository,compileTaskContextFromStore,analyzeImpact,verifyClaims,CartographerQueryService. These take an explicit snapshot id, so callers that own snapshot scoping keep full control.
Documentation
| Document | Contents | | --- | --- | | docs/API.md | The full public contract: lifecycle, requests, responses, schema versions, evidence | | docs/agents.md | Playbook for agent consumers | | docs/CLI.md | Command reference, flags, exit codes, output contract | | docs/architecture.md | The five layers and how they compose | | docs/limitations.md | What is deliberately not covered, and where results are partial | | BENCHMARKS.md | Measured performance, methodology, and what is not measured |
Commands
| Command | Purpose |
| --- | --- |
| npm run check | Type-check source without emitting files |
| npm test | Run the complete test suite |
| npm run build | Emit ESM JavaScript and declarations to dist/ |
| npm run test:coverage | Generate V8 coverage reports |
| npm run cartographer:self-scan | Scan this repository into .cartographer/self-scan/ |
| npm run cartographer:self-scan:check | Validate generated self-scan artifacts |
| npm run bench:public | Reproduce the published benchmarks |
| npm run verify | Run type-check, tests, build, self-scan, and checker |
| npm run verify:evidence | Verify the immutable final real-repository evidence bundle |
Compatibility
The initial standalone release deliberately retains .rectorignore and the wire identifiers rector.cartographer.selfScan.v1, rector.cartographer.selfScanAllowlist.v1, and rector.cartographer.latestFiles.v1. They are compatibility contracts, not runtime dependencies on Rector.
