# ANCI v0.1 DRAFT — Architecturally Normalized Code Index

> **Status:** DRAFT (unstable). Do not depend on this format in production
> consumers without pinning to a specific carto-md version. The wire
> format may change in any direction up to v1.0.
>
> **Spec:** v0.1.0-DRAFT
> **Reference implementation:** carto-md ≥ 2.0.9
> **Editor:** [@theanshsonkar](https://github.com/theanshsonkar)
> **License:** MIT (the spec itself)
> **Repository:** https://github.com/theanshsonkar/carto

---

## 1. Motivation

Every AI coding tool today re-discovers a codebase's architecture from
scratch on every session. Cursor builds its own embedding index. Cline
builds its own. Continue builds its own. The work duplicates across
tools and is lost across sessions.

ANCI fills the hole. It is a **static file format** — two files,
`anci.yaml` and `anci.bin`, sitting at a known location in a repository
— that describes that codebase's architecture in a form any AI tool can
read without indexing it itself.

ANCI is to codebases what OpenAPI is to REST APIs: a standardized way for
something to describe itself to consumers it doesn't know about.

## 2. Goals & non-goals

### Goals

* **Tool-neutral.** No assumption about which AI tool consumes ANCI.
* **Local-first.** ANCI files live next to the code, in `.carto/` by
  convention. Never sent over a network unless the user chooses to.
* **Hybrid representation.** Human-readable header for grep / inspection;
  binary body for fast queries on million-file repos.
* **Bounded size.** Target ≤ 20 MB total for a 100K-file repo.
* **Lossless round-trip.** Producer → file → consumer reproduces the
  exact graph, domains, and metadata.
* **Forward-compatible.** New optional sections may be added in v0.x
  releases. Consumers ignore unknown sections.

### Non-goals

* **Not a query language.** Consumers traverse the data structures with
  their own code. ANCI is just the data.
* **Not a network protocol.** No RPC. No streaming. Static files only.
* **Not a replacement for source code.** ANCI describes structure
  (graph, domains, routes), not source.
* **Not yet stable.** v0.1 DRAFT may break in v0.2, v0.3, etc. Stability
  begins at v1.0.

## 3. File layout

A repository that ships ANCI places two files in `.carto/`:

```
.carto/
├── anci.yaml    # Header. UTF-8 YAML. Required. Human-readable.
├── anci.bin     # Body.   Binary.    Required. Machine-readable.
```

Producers write both. Consumers read either independently:

* If a consumer only needs domain names and route counts, parse
  `anci.yaml` and ignore the body.
* If a consumer needs blast radius or graph traversal, parse the body.

The header MUST point to the body's filename and byte length. If they
disagree, the body is authoritative for graph data and the header is
authoritative for human metadata.

## 4. Header format (`anci.yaml`)

UTF-8, YAML 1.2, **strict subset**:

* 2-space indentation
* `key: value` pairs only (no flow style `{}`/`[]`)
* All string values double-quoted (no implicit typing)
* Lists use `- ` prefix
* No multi-line strings, no anchors, no aliases, no tags

A v0.1.0-DRAFT producer MUST emit a header that conforms to this
schema. A consumer MAY accept other YAML inputs but reference
implementations only emit/parse the strict subset.

### 4.1 Schema

```yaml
anci:
  version: "0.1.0-DRAFT"            # required, string
  generator: "carto-md@2.1.0"       # required, string
  generated_at: "2026-06-07T..."    # required, ISO-8601 UTC
  carto_version: "2.1.0"            # required, bare producer version
  contains:                         # required, capability list (≥1 entry)
    - "structural"
  body:
    file: "anci.bin"                # required, relative to header
    bytes: 12345                    # required, body length on disk
    content_digest: "sha256:9f86…"  # optional, hash of the body bytes

source:                             # required block; fields null if non-git
  commit: "a4334a2c…"               # git HEAD commit SHA, or null
  tree_hash: "e7d3f1…"              # git HEAD tree hash, or null
  branch: "main"                    # current branch, or null (detached)

grammar_versions:                   # optional; { pkg: version }
  tree-sitter: "0.25.0"
  tree-sitter-javascript: "0.25.0"
  tree-sitter-typescript: "0.23.2"

project:
  total_files: 7567                 # required
  total_routes: 86                  # required
  total_models: 12                  # required
  total_import_edges: 13420         # required

domains:                            # required, possibly empty
  - name: "AUTH"
    file_count: 42
    route_count: 7
    model_count: 1

high_impact:                        # optional, top N by transitive dep count
  - file: "src/auth/session.ts"
    transitive_dependents: 47

routes:                             # optional
  - method: "POST"
    path: "/auth/login"
    file: "src/auth/login.ts"
    framework: "express"
    handler: "login"

models:                             # optional
  - name: "User"
    kind: "prisma"
    file: "prisma/schema.prisma"
```

Field names are stable for the v0.x line. Adding new optional fields is
not a breaking change. Removing or renaming a field IS breaking and
requires a version bump.

### 4.1.1 Container identity (v0.1)

The identity fields turn ANCI from "a folder" into a versioned,
verifiable artifact:

| Field                       | Meaning                                                              |
|-----------------------------|----------------------------------------------------------------------|
| `anci.carto_version`        | Bare producer version (distinct from the free-form `generator`).     |
| `anci.contains`             | Capability list — which memory layers the container carries. v0.1 producers emit `["structural"]` (the import graph, domains, routes, models). Episodic/temporal/semantic/procedural layers are **not** in the file; they remain live in the producing engine. |
| `anci.body.content_digest`  | `"<algorithm>:<hex>"` hash of the exact `anci.bin` bytes. Optional but recommended. Consumers recompute it to verify integrity. |
| `source.commit`             | Git commit the container was built from, or `null` (non-git repo).   |
| `source.tree_hash`          | Git tree hash of that commit — identifies exact committed content.   |
| `source.branch`             | Branch name at build time, or `null` (detached HEAD / non-git).      |
| `grammar_versions`          | Map of tree-sitter core + grammar package versions used to extract. Enables reproducibility checks (same repo + same grammars → same digest). |

**Consumer integrity check.** A consumer that reads `content_digest`
SHOULD recompute `<algorithm>(anci.bin)` and compare. On mismatch it
SHOULD warn; a strict consumer MAY refuse to load. The reference
consumer (`loadAnci`) exposes `reader.verifyDigest()` and a
`{ verify: true }` option that promotes a mismatch to a thrown error.

**Staleness.** `source.commit`/`source.tree_hash` let a consumer detect
that the container was built from an older repo state than the current
`HEAD` (e.g. "graph is N commits stale — results may be inaccurate").
The reference engine surfaces this in MCP responses and `carto doctor`.

### 4.2 Versioning

| Field           | Meaning                                                   |
|-----------------|-----------------------------------------------------------|
| `anci.version`  | Spec version. v0.x is unstable. v1.0+ follows semver.     |
| `anci.generator`| Producer identity. Free-form string, conventionally `name@version`. |

Consumers MUST refuse to parse a header whose `anci.version` is not in
their compatibility set. Reference consumer behavior (carto-md):

* Accepts: `0.1.x-DRAFT`
* Rejects: anything else

## 5. Body format (`anci.bin`)

Binary, little-endian, length-prefixed sections. Designed so a single
streaming pass can fully reconstruct all bitmaps, paths, and domain
mappings.

### 5.1 Header

| Offset | Size  | Field        | Value                                         |
|-------:|------:|--------------|-----------------------------------------------|
|      0 |  4 B  | magic        | `0x49434E41` (`'A','N','C','I'` little-endian)|
|      4 |  1 B  | version      | `0x01`                                        |
|      5 |  3 B  | reserved     | zero                                          |
|      8 |  4 B  | size_bits    | u32 — bit dimension of every bitmap (= max file id + 1) |

Total fixed-header length: **12 bytes.**

### 5.2 Sections

After the fixed header, sections appear **in this order**. Each section
begins with a u32 count.

#### 5.2.1 `forward` — file → its imports

```
count_u32
count × {
  from_file_id_u32
  words_len_u32
  words_u32 × words_len      // raw Uint32Array bytes, LE
}
```

Bit `j` of the bitmap of `from_file_id` is 1 iff the file with id
`from_file_id` directly imports the file with id `j`.

#### 5.2.2 `reverse` — file → its direct dependents

Same record shape as `forward`. Bit `j` set iff file `to_file_id` is
directly imported by file `j`.

#### 5.2.3 `popcount` — pre-computed transitive dependent counts

```
count_u32
count × {
  file_id_u32
  count_u32       // # of distinct dependents reachable in ≤ 5 hops
}
```

Sorted DESC by count. Lets `getHighImpactFiles(n)` answer in O(1).
Producers MUST clamp BFS to 5 hops to match the carto-md reference.

#### 5.2.4 `paths` — file id → path string

```
count_u32
count × {
  file_id_u32
  path_len_u32
  path_bytes (UTF-8, no NUL terminator)
}
```

Paths are project-relative POSIX paths (forward slashes, no leading `./`).

#### 5.2.5 `file_domain` — file id → domain id

```
count_u32
count × {
  file_id_u32
  domain_id_u32
}
```

Files absent from this section have no domain assignment. A file MAY
have at most one domain.

#### 5.2.6 `domain_names` — domain id → human name

```
count_u32
count × {
  domain_id_u32
  name_len_u32
  name_bytes (UTF-8)
}
```

Domain names are uppercase by convention (`AUTH`, `PAYMENTS`,
`DATABASE`) but the spec does not constrain their contents.

### 5.3 Total layout (concrete byte map)

```
[ 12 B fixed header ]
[ forward section     ]
[ reverse section     ]
[ popcount section    ]
[ paths section       ]
[ file_domain section ]
[ domain_names section]
EOF
```

A consumer that knows the spec version can stream-parse top to bottom.
There is no out-of-line index — the format is designed for one
sequential read.

## 6. Generation algorithm (reference)

Producers SHOULD compute the body as follows:

1. Assign each file a stable, dense integer id. For **reproducibility**
   (CT-4), ids MUST be assigned over the file list sorted by normalized
   POSIX path (forward slashes, code-unit order — never a locale-aware
   comparison), NOT over the filesystem enumeration order. This makes the
   body layout, and therefore `content_digest`, identical across machines
   for the same repo. See §16.2.
2. For each file, build a bitmap of its forward imports (resolved
   imports only; unresolved imports are excluded).
3. Reverse the forward bitmaps into `reverse`.
4. For each file with ≥ 1 dependent, run a 5-hop BFS over `reverse`
   using the same self-loop guard as `bitmap/sidecar.js`:
   a. Seed `visited` with direct dependents.
   b. Expand frontier by one hop, mask `visited`, add result to
      `visited`. Repeat to depth 5 or until frontier is empty.
   c. Clear the seed bit (cycles must not count the file itself).
   d. Record `popcount(visited)` if > 0.
5. Sort `popcount` records DESC.
6. Write sections in the order specified in §5.2.

The carto-md reference implementation lives in `src/anci/serialize.js`.

## 7. Consumer responsibilities

A consumer MUST:

* Validate magic bytes and version before reading any section.
* Refuse to parse if `size_bits` differs from the bitmap dimension
  derived from any section's `words_len`.
* Treat unknown sections (in future v0.x releases) as opaque and skip
  them. The reference implementation enforces strict order today, but
  consumers SHOULD be permissive on order to ease forward compatibility.

A consumer SHOULD:

* Cache the parsed body in memory rather than re-parsing on every
  query. The format is designed for fast load, not fast random seek.
* Cross-validate the header's `body.bytes` against the actual file
  size and warn (not fail) on mismatch.

## 8. Versioning & deprecation policy

### v0.1.0-DRAFT (this spec)

* Format may change in any direction up to v1.0.
* Consumers pin to specific producer versions.
* Reference implementation is carto-md ≥ 2.0.9.
* No backward compatibility guarantees.

### Future v0.x (e.g. v0.2)

* MAY add optional header fields.
* MAY add optional body sections (appended after `domain_names`).
* MUST NOT remove or rename existing fields.
* MUST bump `anci.version` to a new minor.
* Reference consumer accepts the latest v0.x and the version it shipped
  against.

### v1.0 stability

When the format is judged ready (reference partner integrations exist,
plus 6 months of v0.x stability), `anci.version` graduates to `1.0.0`
and standard semver applies thereafter:

* MAJOR: incompatible breaking change.
* MINOR: backward-compatible addition.
* PATCH: clarification only.

A v1.x consumer MUST accept all v1.x bodies. v0.x compatibility at v1.0
is not guaranteed — producers re-emit at the v1.0 schema.

## 9. Security considerations

ANCI is a passive description of code structure. It contains:

* File paths (project-relative).
* Domain names.
* Route paths and HTTP methods.
* Model names and kinds.
* Import graph as integer bitmaps.

ANCI does **not** contain source code, secrets, env values, or
user-identifying information. Producers MUST NOT include source content
in any field.

A repository that publishes its `.carto/anci.{yaml,bin}` to a remote
location is exposing its architecture. This is the same disclosure
level as making the directory tree public. Producers SHOULD respect
`.cartoignore` and SHOULD NOT include paths matching its patterns.

## 10. Why YAML + binary (and not just YAML, or just binary)

| Property               | YAML alone | Binary alone | Hybrid |
|------------------------|:----------:|:------------:|:------:|
| Human-readable         | ✅          | ❌            | ✅      |
| Grep-able from CI      | ✅          | ❌            | ✅      |
| Compact at 100K files  | ❌ (50–500 MB) | ✅       | ✅ (5–20 MB) |
| Fast graph queries     | ❌          | ✅            | ✅      |
| Easy partial parse     | ✅          | ⚠️           | ✅      |

The split mirrors the structure of the data: metadata is small and
varied (good for YAML); the graph is large and uniform (good for
binary).

## 11. Why these specific bitmap sections

The body ships exactly the data needed to answer the four MCP tools
that drive most AI-tool queries:

* `getBlastRadius(file)` → walk `reverse` BFS-style.
* `simulateChangeImpact(files)` → OR-aggregate `reverse` bitmaps.
* `getHighImpactFiles(n)` → take first N from `popcount`.
* `getDomainsList()` / domain membership → header + `file_domain` +
  `domain_names`.

Everything else (cross-domain detection, similarity, change-plan
ranking) can be derived from these primitives. The format keeps the
ship surface small.

## 12. Compatibility with carto-md `bitmap.bin`

ANCI v0.1 and carto-md's internal `bitmap.bin` (magic `0x54524243`
"CBRT") share record-level encoding but are **distinct file formats
with distinct magics**. Reasons:

* `bitmap.bin` is private cache; `anci.bin` is public export.
* `bitmap.bin` includes `crossForward` and `domainBitmaps` (internal
  query optimizations); ANCI omits them. Consumers can re-derive both
  in O(N) on load.
* `bitmap.bin` may evolve at carto-md's pace; ANCI evolves only at
  spec-revision pace.

A consumer MUST NOT attempt to load `bitmap.bin` as ANCI or vice versa.

## 13. Reference vs. interoperability tests

The carto-md reference implementation includes a roundtrip test suite
(`test/test.js` → `ANCI roundtrip`). Other implementations are
encouraged to:

* Run carto-md's reference suite against their own
  serialize/deserialize code.
* Publish their own conformance tests for the spec to consume.

The DRAFT spec deliberately ships **without** an independent
conformance harness. v1.0 will require one before formalization.

## 14. Open questions (RFC-stage feedback welcome)

* **Should the body be Roaring-compressed instead of dense
  Uint32Array?** Roaring is smaller on sparse repos but adds a
  dependency. v0.1 ships dense; v1.0 may add an opt-in compressed
  encoding.
* **Should env vars be in the header?** Currently no — they leak
  configuration surface. Open question for partners.
* **Should source language be part of `paths`?** Currently no — language
  is derivable from extension. Open if consumers ask.
* **Cross-repo references?** Out of scope for v0.1. Tier 3 work.

## 15. Acknowledgements

Inspired by OpenAPI's standardization play (2010-2015), Git's pack
reachability bitmaps (2013), and Lucene's Roaring posting lists (2014).

The spec is iterated alongside carto-md. File issues at
https://github.com/theanshsonkar/carto for proposed changes.

## 16. Single-file container envelope (`.anci`) — CT-3

The two-file layout of §3 (`anci.yaml` + `anci.bin`) is the on-disk form.
For **transport** — handing a container from one machine to another —
`carto export` packs both files into ONE portable file, conventionally
`project.anci`, and `carto load` unpacks it back into a queryable
`.carto/` with **no re-index**. This makes "build once, load anywhere"
literal: build on machine A, copy `project.anci`, `carto load` it on
machine B, and blast radius answers instantly against the container A
produced.

### 16.1 Envelope wire format (little-endian)

```
magic        u32  = 0x50434E41   ("ANCP" — 'A','N','C','P' little-endian)
version      u8   = 1
reserved     u8×3 = 0
entry_count  u32
entry_count × {
  name_len   u32
  name       name_len UTF-8 bytes     # a bare basename (see 16.3)
  data_len   u32
  data       data_len bytes
}
trailer      32 bytes = sha256(all preceding bytes)
```

The envelope has its **own magic** (`ANCP`), distinct from the ANCI body
magic (`ANCI` = `0x49434E41`) and carto-md's internal bitmap cache
(`CBRT`). A consumer MUST NOT attempt to load one as another. A v0.1
container carries exactly two entries — `anci.yaml` and `anci.bin` — in
name-sorted order.

### 16.2 Reproducibility

Entries are written in a fixed (name-sorted) order, so packing the same
input pair twice yields a byte-identical envelope. Note that `anci.yaml`
embeds a `generated_at` timestamp, so the *envelope file* is only
byte-stable for a fixed input pair. The **reproducible identity** of a
container is its `anci.bin` `content_digest` (§4.1.1), which the envelope
carries verbatim inside the packed `anci.yaml`.

`anci.bin` itself is reproducible: **the same repo + the same grammar
versions produce the same `content_digest`, independent of the machine or
filesystem.** This holds because the producer assigns file ids over a
list sorted by normalized POSIX path (not filesystem enumeration order),
so the body layout — and therefore its hash — is a true content address
of the repo. `grammar_versions` (§4.1.1) records the tree-sitter versions
used, so a consumer that sees two containers with different digests can
tell whether a grammar version drift could explain the difference. This
enables container **diff / verify** across machines and over time.

### 16.3 Security — a shared container is UNTRUSTED

A `.anci` file is shareable, so on ingest it MUST be treated as untrusted.
The reference `carto load` / `unpackContainer` enforce, in order:

1. **Integrity.** Recompute the trailer `sha256` over the whole payload
   before trusting any length field. A mismatch (corruption, truncation,
   tampering of the envelope) rejects the file.
2. **Entry whitelist.** Only the two known basenames (`anci.yaml`,
   `anci.bin`) are accepted; any other name rejects the container.
3. **Path-traversal guard (zip-slip).** An entry name must be a bare
   basename — no path separators, no `..`, no absolute path, no NUL — so
   unpacking can never write outside the destination directory. Writes
   additionally re-derive `path.basename` and assert containment (defense
   in depth).
4. **Bounded allocation.** Entry count and per-entry size are capped so a
   hostile header cannot trigger an unbounded allocation.

Beyond structural validation, the **contents** of a loaded container —
file paths, domain names, route strings, and any other embedded text —
are **data, never instructions.** The consumer (`loadAnci`) parses them
into data structures with a strict hand-rolled YAML subset parser and the
binary deserializer; nothing in a container is ever executed, `eval`-ed,
or interpreted as a command or a prompt. A container that embeds
prompt-injection-looking or shell-injection-looking strings loads those
strings verbatim as literal string values. Any tool consuming ANCI SHOULD
preserve this property: render container text as untrusted data.

This guardrail ships **with** single-file export/import (CT-3), before any
signing or registry work (Tier 3). Signing (CT-7) later adds *origin*
verification on top; the untrusted-data handling is independent and
earlier.
