the format
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, 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.
Also served as Markdown at /spec.md.
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/docspackcan 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.
@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.mdllms.txt at the package root is the table of contents, in the 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.
# @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 keydocspack 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. |
{
"$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
fileresolves 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.jsonversion 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>. -
tokensis 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 —
additionalPropertiesis true at every level. An invented field is not an error, so it is also not a feature: nothing reads it.docspack doctorwarns about keys outside the set and--strictfails 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.
# 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.
tokensin 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 buildwarns when a rebuild drops an id the previous payload published. - The derivation
docspack builduses 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 buildappends-2,-3and warns, because which of the two takes the bare id depends on the order the files were read in.
docs/
├── cli.md ## Casen → cli-casen
└── cli/
└── casen.md (intro) → cli-casen ← the same idDeriving 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, andtags, the chunk'stagsandentitiesjoined 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 fixedk1 = 1.2andb = 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
--limitchunks (3) are taken in rank order, then kept while the running total oftokensstays 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:
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:
## 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 — the JSON Schema, versioned with the format
- /spec.md — this page as Markdown, for an agent
- Authoring a package — writing one with the CLI
- Verifying documentation against code — what
docspack verifychecks, and what it will not - /llms.txt — the agent-facing summary of all of it