# Citation and portability

Evidence should survive being copied between agents, pasted into documents, and read years later. Every object therefore carries a `cite_as` block that anyone can check without trusting whoever passed it along.

```json
{
  "id": "prop_nyc_sidewalk_city_not_liable",
  "type": "proposition",
  "version": 1,
  "url": "https://agents.newyorkeverydaylawreference.com/proposition/prop_nyc_sidewalk_city_not_liable",
  "content_hash": "sha256:…",
  "dataset_version": "2026.09.29"
}
```

Authorities, cases, and sources also carry `citation`: the legal citation (for example `Vucetovic v Epsom Downs, Inc., 10 NY3d 517`).

## Identifiers

IDs are permanent and typed by prefix (`prop_`, `auth_`, `case_`, `src_`, `rel_`, `pkt_`). An ID is never renamed or reused. When the substance of a record changes, its `version` goes up, the old version is archived, and `revisions` explains what changed.

## Canonical URLs

- `/{type}/{id}` is always the current version.
- `/{type}/{id}/v/{n}` is version `n`, which never changes. Cite this form when you need to point at exactly what you read.
- `.json` and `.md` suffixes pick a representation; the same object and hash either way.

## Content hashes

`content_hash` is `sha256:` plus the hex SHA-256 of the **canonical JSON** of the stored record. To recompute it:

1. `GET {url}.json` (the full view, not `?view=compact`).
2. Delete the derived keys: `url`, `links`, `trust`, `cite_as`, `graph`, `applicability`, `provenance`.
3. Serialize as canonical JSON: object keys sorted by code point, no whitespace, `undefined` omitted, arrays in order, strings and numbers as `JSON.stringify` writes them.
4. SHA-256 the UTF-8 bytes.

In JavaScript:

```js
const DERIVED = ["url", "links", "trust", "cite_as", "graph", "applicability", "provenance"];
const canonical = (v) =>
  v === null || typeof v !== "object" ? JSON.stringify(v)
  : Array.isArray(v) ? `[${v.map((x) => canonical(x ?? null)).join(",")}]`
  : `{${Object.keys(v).filter((k) => v[k] !== undefined).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`;

const record = await (await fetch(`${citeAs.url}.json`)).json();
for (const key of DERIVED) delete record[key];
const hex = Buffer.from(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical(record)))).toString("hex");
console.log(`sha256:${hex}` === citeAs.content_hash);
```

Trust fields and relationships are derived, so they can change (for example when a new case limits a proposition) without changing the hash of the proposition itself. The hash covers what the record says, not what the rest of the dataset says about it.

Each passage in `provenance.passages` also has its own `sha256` over the exact passage text, so a quotation can be checked on its own.

## Packets

A packet's ID is derived from its normalized request and the dataset hash, so the same question against the same data always gives the same `packet_id`. `provenance.packet_hash` is the SHA-256 of the canonical packet body without `packet_hash` itself. Packets contain no timestamps.

## Resolving

`POST /resolve` (or the `resolve_citation` MCP tool) takes an ID, a URL, or a legal citation, plus an optional `content_hash`:

| `status` | Meaning |
| --- | --- |
| `current` | You hold the current version (and the hash matches, if you sent one) |
| `revised` | A newer version exists; `current` has its `cite_as` |
| `superseded` | Another rule replaced this one from a date; `superseded_by` lists it. The old rule still describes earlier events |
| `hash_mismatch` | The version exists but the content differs from what you hold; don't rely on your copy |
| `version_not_found` | The ID exists but not that version |
| `not_found` | Nothing matches. For a legal citation this may mean it isn't in this dataset, or that it doesn't exist |

`resolve` answers as of today. If you hold a claim about an earlier date, `superseded` is expected when the replacement took effect after that date: compare each `superseded_by[].valid_from` with your `as_of`. The handoff demo does exactly this.

A legal citation to a whole section also finds a record for one of its subsections: `Administrative Code § 7-201` resolves to the record for § 7-201(c).

`not_found` is how you catch a fabricated citation. In the 2026 *Landberg v City of New York* appeal, a separate decision sanctioned counsel for a brief citing "Xiang Fu Ji v City of New York (13 NY3d 791)", a case that does not exist. Resolving `13 NY3d 791` here returns `not_found`. (A `not_found` alone doesn't prove a citation is fake, since coverage is selective, but it tells you to check before relying on it.)

## Offline use

Everything needed to check a citation is in the `cite_as` block and the JSON at its URL. Mirrors can serve the same JSON under a different origin; the hash stays the same because it doesn't cover `url`.

`npm run build` writes a static copy of every page and every record version to `dist/`, with `manifest.json` (every `id@vN` and its `content_hash`) and `dataset.json` (every stored record, for bulk download). Each dataset release is tagged `dataset-{version}` in git, and `data/published.lock.json` holds the hash of every published version, so a published `id@vN` can be shown never to have changed.
