# The docspack package format

A documentation package is an ordinary npm package whose payload is Markdown. Everything a publisher writes and a consumer reads is on this page; the machine-readable form is [/schema/v1.json](/schema/v1.json), which is what `$schema` in every generated manifest points at.

`docspack build` writes all of this for you. The format is documented because a package is publishable without the CLI, and because a consumer other than docspack should be able to read one.

## Naming

Three name shapes are discoverable, and discovery is a pure name check against `package.json` — no registry call, no manifest fetch, nothing that can fail while offline.

- `@vendor/docspack` — published by whoever owns the scope. Trusted: its content is presented to an agent as documentation.
- `@vendor/<name>-docspack` — a second pack in the same scope, trusted on the same grounds. Whoever can publish `@vendor/docspack` can publish this and nobody else can.
- `@docspack-community/<name>` — published by anyone. Every answer drawn from one is labelled `(community)` and marked untrusted data.

A vendor documenting its own products needs one pack, and `@vendor/docspack` is the name to use: it is the only one derivable from a dependency's own name, so it is the only one `docspack init` suggests and the only one worth guessing. The suffixed sibling is for the case a single pack cannot hold — a vendor that also **redistributes somebody else's documentation**, where licence, version, release cadence and attribution belong to a different upstream. Merging those into one package relicenses one corpus under the other's terms and version-locks neither. Derive to suggest; match to discover.

A dependency that ships `.llms/manifest.json` under any other name is not indexed — the name check is what decides whether a package may put content in front of a model, and a `docspack` field or a manifest alone would let any transitive dependency opt itself in. It is reported instead: `docspack sync` and `docspack list` name it, say how many chunks its manifest declares, and say why it was skipped, and `docspack doctor` fails on it.

## Layout

`.llms/` is the payload and must be listed in the package's `files`. Omitting it publishes a package that installs cleanly and indexes nothing — `docspack doctor` treats that as an error.

```text
@vendor/docspack/
├── package.json      name, version, and the docspack build settings
├── llms.txt          human-readable table of contents
└── .llms/
    ├── manifest.json every chunk, its file, size, tags and entities
    └── chunks/
        ├── overview.md
        └── webhooks-signing.md
```

`llms.txt` at the package root is the table of contents, in the [llmstxt.org](https://llmstxt.org) shape: an `H1` naming the package, an optional `>` summary, then one or more `##` sections of Markdown links. Nothing in the index reads it — `manifest.json` is the machine copy — so it is what a person or an agent browsing `node_modules` opens first.

```md
# @acme/docspack

> Documentation package for @acme/docspack. Generated by docspack; the
> machine-readable payload is in .llms/.

## Chunks

- [webhooks-signing](.llms/chunks/webhooks-signing.md): webhooks, signature, security
- [api-auth](.llms/chunks/api-auth.md): authentication, bearer, api key
```

`docspack build` writes one link per chunk, annotated with its first six tags. A hand-written file may say more; it may not say less than where the chunks are.

## manifest.json

The one file a consumer must read. It is the whole index of the package: a reader budgets a response from `tokens` before opening a single chunk file.

| Field | Type | | Meaning |
| --- | --- | --- | --- |
| `name` | string | required | npm package name, e.g. @stripe/docspack. |
| `version` | string | required | Must match the version in package.json. |
| `documents` | array |  | Libraries this package documents, each "name" or "name@version". An array even for one. |
| `chunks` | array | required | Every retrievable unit of documentation in the package. |

### Each entry in `chunks`

| Field | Type | | Meaning |
| --- | --- | --- | --- |
| `id` | string | required | Unique within the package; used to build the chunk_id in the index. |
| `file` | string | required | Path relative to .llms/. Must not escape that directory. |
| `tokens` | integer |  | Approximate token count, used to budget a response. Omit it to have it estimated; 0 is not a value. |
| `tags` | array |  | Search terms indexed alongside the content. |
| `entities` | array |  | Identifiers the chunk documents, e.g. Stripe.setApiKey. |
| `documents` | array |  | Libraries this chunk describes, overriding the package's documents. For a monorepo documenting many libraries at many versions from one surface. |

```json
{
  "$schema": "https://docspack.dev/schema/v1.json",
  "name": "@acme/docspack",
  "version": "1.4.0",
  "documents": ["acme@1.4.0"],
  "chunks": [
    {
      "id": "webhooks-signing",
      "file": "chunks/webhooks-signing.md",
      "tokens": 412,
      "tags": ["webhooks", "signature", "security"],
      "entities": ["client.verifySignature", "WebhookEvent"]
    }
  ]
}
```

`documents` is what lets an answer say which release it describes. A package version cannot always imply it: a repository publishing eighteen libraries at four versions from one documentation surface has no single version to mirror. `docspack ask` prints it under each hit, and `docspack verify` checks the chunks' entities against every library named there.

A chunk may carry its own `documents`, and then that is what it describes. On the package the field is the union — true of the pack and useless about any one chunk — so a monorepo names the library per chunk, and an answer, and `verify`, narrow to it. Absent on a chunk, the package's list stands.

## Rules a consumer enforces

- A chunk `file` resolves inside `.llms/` or it is refused. It must not escape that directory. A manifest is third-party input, so this is a security boundary rather than a convenience check.
- The installed `package.json` version wins over the manifest's when the two disagree, and the mismatch is reported.
- Chunk ids are unique within a package and match `^[A-Za-z0-9][A-Za-z0-9._-]*$`. The id an answer is headed with is `<name>@<version>/<id>`.
- `tokens` is omitted or at least 1. It is never 0: absent means "estimate it", and a chunk that is genuinely empty is a different error.
- Unknown fields are accepted and ignored — `additionalProperties` is true at every level. An invented field is not an error, so it is also not a feature: nothing reads it. `docspack doctor` warns about keys outside the set and `--strict` fails on them, because a publisher is the last party who can still tell a typo from an extension.

## Chunk files

A chunk file is Markdown, and it is the one part of a package that reaches a model unaltered: a consumer returns its text verbatim. Nothing strips a heading, resolves a relative link or rewrites a code fence.

````md
# Verifying a webhook signature

<!-- docspack: from docs/webhooks.md -->

Call `client.verifySignature(payload, header)` before trusting a delivery. It
throws `SignatureError` when the header is absent.

```ts
client.verifySignature(await request.text(), request.headers.get("acme-signature"));
```
````

- It SHOULD open with one `#` heading naming the section. It is the line an agent reads first, and the only context the chunk has once it is lifted out of the document it came from.
- It SHOULD then carry its provenance as `<!-- docspack: from <path or URL> -->`. An HTML comment renders as nothing, survives being pasted into a context window, and is what lets a reader say where a claim came from.
- It carries no front matter. Front matter is an authoring control on a source document and is consumed when the package is built.
- The rest is ordinary Markdown. `tokens` in the manifest measures this file, heading and comment included.

One idea per file is the whole design. A chunk that answers two questions is returned for both and spends the budget twice; `docspack doctor` warns above ~1,500 tokens and below ~30.

## Chunk ids

An id matches `^[A-Za-z0-9][A-Za-z0-9._-]*$` and is unique within its package. It is a public name, not an implementation detail: an answer is headed with `<name>@<version>/<id>`, and `docspack feedback add --chunk @acme/docspack@1.4.0/api-auth` pins one.

- Ids SHOULD be stable across versions. Renaming a chunk is a breaking change to the package: it orphans every finding filed against the old id and every link that quoted it. `docspack build` warns when a rebuild drops an id the previous payload published.
- The derivation `docspack build` uses is the document title and the section heading, slugified and joined — lowercased, every run of non-alphanumerics collapsed to `-`, trimmed to 60 characters — or the title alone when the heading is the title.
- That derivation collides, and uniqueness does not fall out of it. A generator must detect a collision and resolve it deterministically. `docspack build` appends `-2`, `-3` and warns, because which of the two takes the bare id depends on the order the files were read in.

```text
docs/
├── cli.md          ## Casen   →  cli-casen
└── cli/
    └── casen.md    (intro)    →  cli-casen   ← the same id
```

Deriving an id from a heading is what makes editing a heading rename a chunk. Where that matters, write the id rather than deriving it: the manifest is the authority, and nothing requires an id to resemble the text it points at.

## Ranking and the response budget

Two conforming implementations should return the same three passages for the same query, so the retrieval is specified rather than left to the reader.

- The index is SQLite FTS5 with `tokenize="porter unicode61"`, over two searchable columns: `content`, the chunk file's text, and `tags`, the chunk's `tags` and `entities` joined by a space.
- Results are ordered by `bm25(chunks_fts, 1.0, 3.0)` — content weighted 1.0, tags and entities 3.0 — using FTS5's own bm25 with its fixed `k1 = 1.2` and `b = 0.75`. bm25 normalizes for length, so a short chunk outranks a long one on the same match.
- A query is lowercased and split on letters and digits. Terms of one or two characters are dropped, then closed-class function words; each surviving term is quoted and joined with `OR`. Each narrowing falls back to the one before it, so a query made entirely of short or common words still searches for something.
- Up to `--limit` chunks (3) are taken in rank order, then kept while the running total of `tokens` stays within `--max-tokens` (3,000). The first hit is always returned, even alone over budget. The budget is spent from the manifest, before any chunk file is opened.

`tokens` may be omitted to have it estimated, so a publisher's count and a consumer's estimate have to agree on what they are counting. The reference estimate is the trimmed file contents in characters, divided by four, rounded up, and never below 1:

```ts
tokens = Math.max(1, Math.ceil(contents.trim().length / 4))
```

It is deliberately not a tokenizer. Its only job is to be the same number on both sides of a publish, so the 3,000-token cap means one thing everywhere; a real tokenizer would make it mean one thing per model.

## Authoring controls

Front matter carries `title`, `tags` and `documents` for a whole document. A single section takes its own with an HTML comment under the heading, which is what to reach for when one page holds eighty of them:

```md
## Two-column layout

<!-- docspack: tags=grid,columns -->
<!-- docspack: entities=TwoColumn -->
<!-- docspack: documents=@acme/react@0.17.0 -->
```

Tags are weighted above prose in the ranking, so they are the lever that aims one chunk at one question. A directive's tags are indexed as written; words taken from a heading have function words filtered out of them first, because a section titled "How to use it" otherwise outscores the corpus on every question beginning "how do I".

A directive may also narrow what one section documents, which is the per-chunk `documents` above. Both other levers are in the ranking: tags aim a chunk, and length decides how far it carries.

## Next

- [/schema/v1.json](/schema/v1.json) — the JSON Schema, versioned with the format
- [/spec.md](/spec.md) — this page as Markdown, for an agent
- [Authoring a package](/docs/authoring-a-package.md) — writing one with the CLI
- [Verifying documentation against code](/docs/verify.md) — what `docspack verify` checks, and what it will not
- [/llms.txt](/llms.txt) — the agent-facing summary of all of it
