Authoring a documentation package
A documentation package is a normal npm package whose payload is Markdown rather than code.
docspack init scaffolds one, docspack build regenerates it, docspack doctor checks it
and docspack preview shows what an agent would get back.
Starting from nothing
Run init inside the library you want to document:
npx docspack initIt reads the surrounding project first — the library name and version, a docs/ directory,
an OpenAPI document, the git remote, the license — and proposes a package built from what it
found. On a terminal it asks six questions, each pre-filled, so the common path is six times
Enter. In CI or a script, --yes takes the same defaults without asking:
npx docspack init --name @acme/docspack --from ./docs --yes--dry-run prints the file tree and writes nothing. Files that already exist are kept
unless you pass --force, so re-running init in a real project is safe.
What init writes
docspack/
├── package.json name, version, build settings, publish scripts
├── README.md written for whoever installs the package
├── .gitignore the generated payload is a build output
└── .github/workflows/publish-docspack.ymlTwo lines in the generated package.json matter more than the rest. files includes
.llms, because omitting it publishes a package that installs fine and indexes nothing.
And prepublishOnly runs docspack build && docspack doctor --strict, so a stale or broken
payload cannot reach the registry by accident.
When no documentation is detected, init seeds docs/ with three templates shaped the way
retrieval works: one heading per question. They contain TODO: markers, and doctor --strict fails while those remain — a half-filled template cannot be published as though it
were documentation.
Build settings live in package.json
init writes a docspack key:
"docspack": { "documents": "@acme/sdk@1.4.0", "from": "../docs", "maxChunkTokens": 800 }docspack build with no arguments reads it, which keeps the build script, the release
workflow and prepublishOnly from drifting apart. Paths are relative to the package
directory. Command-line flags always win over the key.
Supported fields: documents, from, openapi, source, feedback, maxChunkTokens,
minChunkTokens and pages. feedback is how you opt in to receiving documentation
problems — see the chunk on trust and safety. init does not write it, because opting you in
by default would defeat the point.
What the package documents
documents names the libraries a package describes, each optionally with a version:
"docspack": { "documents": ["@acme/core@0.17.0", "@acme/react@0.17.0", "@acme/i18n@0.14.2"] }Keeping the docs package version equal to the library version is the simplest way to scope a release, and it only works when there is one library. A monorepo publishing eighteen packages at four versions from one documentation surface has no single version to mirror, which is what the list is for.
build writes it into .llms/manifest.json, so it is part of the payload rather than build
configuration a consumer never sees. Every answer then states it:
## @acme/docspack@2026.1.0/button-props
Source: @acme/docspack@2026.1.0 — chunks/button-props.md · documents @acme/core@0.17.0Without it, an agent has to assume the docs package version is the library version. State it instead.
Checking that the docs still describe the code
docspack verify compares the identifiers your chunks name against what the libraries
actually declare. It reads documents from the manifest to know which libraries those are,
and checks against their combined type surface — a name belongs to whichever of them exports
it:
npx docspack verifydrift @acme/docspack@1.4.0 documents @acme/sdk@1.4.0 · 31 names checked, 30 declared
client.setKey() is not declared; did the API become setApiKey?
@acme/docspack@1.4.0/api-auth · chunks/api-auth.mdIt reads the library’s .d.ts files and never imports the package, so nothing you depend on
executes. Exit code 1 means something was reported, which makes it usable in CI beside
doctor. What it checks, what it deliberately leaves alone, and how to scope one chunk to one
library are in verifying documentation against code.
verify reports a name only when it is absent and something very like it is declared —
the signature of a rename. An absent name with no close relative is far more often an
example about a different library, so it is counted and ignored. Measured against 69
published packages, that rule reported nothing at all, while catching 99.9% of simulated
qualifier renames such as setKey becoming setApiKey.
Findings stay on your machine. docspack has no code that can send them anywhere.
Building from Markdown
npx docspack build --from ./docs --name @acme/docspack --pkg-version 1.4.0The generator splits each document at ## headings. If a section is still larger than the
chunk budget it splits again at ###, and then at paragraph boundaries, until every chunk
fits. Retrieval works best when one chunk answers one question, so headings are worth
writing with that in mind.
Tables are written with their alignment padding collapsed. A generated props table padded into
columns measures four times its real weight, because tokens are counted from characters — and
that weight is spent on a reader’s response budget, counted against chunk-too-large, and can
force a table to be split into fragments that then compete for the same query. Collapsing it
changes nothing for a reader or a Markdown renderer. Tables inside a code fence are left
exactly as written.
Generated reference: packing instead of splitting
One chunk per heading is right for prose and wrong for generated reference, where a heading is
a field name. An API generator emits pages shaped Install, Category, Variants, Sizes,
Props, Examples — Category is one line and Sizes is three. Split per heading across two
hundred component pages, that is hundreds of chunks flagged chunk-too-small, plus a
duplicate-chunk for every near-identical Sizes section, and doctor --strict then blocks
the publish.
minChunkTokens packs the other way first:
npx docspack build --from ./reference --min-chunk-tokens 400 --max-chunk-tokens 900Adjacent sections are merged until a chunk reaches the floor, and never past the ceiling. A
whole component lands in one chunk, headed by the page title, with each section’s own heading
kept inside it. Nothing is packed when minChunkTokens is unset, so prose keeps splitting at
every heading.
Each chunk records the heading words as tags, and the identifiers appearing in inline code
as entities. Both member accesses (Client.setApiKey) and bare identifiers written as two
or more words (TwoColumn, useSlide, text-accent) count, so a component library’s API
is indexed and checkable by verify like any other. Both are indexed, which is why a chunk
can be found by an API name that appears only in a code sample.
Aiming one section
Front matter title and tags belong to a whole document. A single section takes its own
with a 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 -->The comments are removed from the generated chunk. Directive tags are added before the heading words, and tags outweigh prose in the ranking, so this is the lever that makes one chunk the answer to one question.
Building from an OpenAPI document
npx docspack build --openapi ./openapi.jsonEvery operation becomes one chunk carrying everything needed to make the call: the base URL,
the credential, the inputs with their types, the body shape, the response and the failures.
Add --from as well to publish prose and an API in one package. JSON or YAML, decided by what
the file holds rather than by its extension.
The whole of it — what a chunk contains, how to ask for one endpoint, and what is not read — is in documenting an HTTP API.
Building from a published llms.txt
Vendors are only beginning to publish documentation packages. To try docspack against a library that has not, build a package from the project’s public llms.txt:
npx docspack sources
npx docspack init --mirror hono --name @docspack-community/hono --yesThis fetches over the network, unlike every other command. It is an authoring convenience,
and the result is a mirror: publish it under @docspack-community, not as the vendor’s own
@vendor/docspack. A vendor redistributing another project’s documentation alongside its own
has a third option: @vendor/<name>-docspack, a second pack in a scope it already owns, which
keeps the two corpora separately licensed, versioned and attributed.
Checking the package
npx docspack doctordoctor reads the package the way the indexer and a reviewer would, and reports what would
otherwise fail silently:
-
errors: an invalid manifest, a missing or empty chunk, a chunk path escaping
.llms/, a version that disagrees withpackage.json, or afilesarray that would publish an empty package -
warnings: chunks too large to fit a response budget or too small to answer anything, chunks with no tags or entities, template placeholders, and a payload older than its source
-
notes: prose style — narration a model does not need, and sentences over forty words
--strict turns warnings into failures, which is what prepublishOnly and CI use. It does
not fail on prose style: --strict is the gate init scaffolds, and documentation written
by hand is not a reason to block a first publish. --pedantic is --strict plus the prose
notes, for authors who want the tool to hold that line. --json prints the findings for a
machine.
Seeing what an agent would get
npx docspack preview "how do I authenticate"preview indexes the package in memory and answers through the same ranking and token
budget an agent gets — without publishing, installing, or writing to the global store. Most
authoring mistakes are obvious the moment you see what comes back: a heading that should
have been two, or a chunk with no distinguishing words in it.
Measuring retrieval, and gating on it
preview answers one query. eval answers a set of them and reports a number:
npx docspack eval ./eval/queries.json --min-hit-rate 90The set is a JSON array of questions and the chunk ids that would answer them:
[
{ "query": "how do I verify a webhook signature", "expect": "webhooks-signing" },
{ "query": "rate limits", "expect": ["rate-limits", "errors"] }
]1 how do I verify a webhook signature
3 rate limits
expected rate-limits | errors
returned charges, webhooks-signing, rate-limits
top-3 2/2 (100%) top-1 1/2 (50%) ~1453 tokens per answerThis is the only check that can fail on retrieval. Every chunk in a package can be well-formed,
well-tagged and the right size while the package answers the wrong question — doctor reads
structure and cannot see it. --min-hit-rate exits 1 below the threshold, which makes it a CI
gate beside doctor --strict, and the mean answer size is what tells you whether a change to
maxChunkTokens bought anything.
Every command’s flags are on its own help page
npx docspack build --help
npx docspack doctor --help
npx docspack eval --helpThe global docspack --help lists the commands and the options common to all of them. What
each command takes is on its own page.
The loop
npx docspack init # scaffold, build and check
$EDITOR docs/02-getting-started.md # write
npx docspack build # regenerate
npx docspack doctor # what is still wrong
npx docspack verify # do the docs still match the code
npx docspack preview "how do I authenticate"
npx docspack eval ./eval/queries.json # does it still answer
npm publish # prepublishOnly rebuilds and checks againWith one library, keep the package version equal to the library version it documents, so
@acme/docspack@1.4.0 describes acme@1.4.0. The generated workflow does this for you on
every GitHub release. With more than one, or with a version that cannot line up, declare
documents and let the manifest carry the mapping.
Fixing documentation you have already published
Publish a new version of the documentation package. A documentation fix is never a reason to
release the library again. It also never reaches anyone without a version bump: npm versions
are immutable, and docspack sync keys the index on name and version.
A consumer’s report reaches you as a GitHub issue, from docspack feedback submit on their
machine. The fix is the ordinary loop, ending in a bump:
$EDITOR docs/03-authentication.md
npm version patch # 1.4.0 becomes 1.4.1
npm publish # prepublishOnly rebuilds and checks againA consumer on ^1.4.0 resolves 1.4.1 on the next install. docspack sync indexes it as a
new entry and leaves the old one for projects still on it. Neither side needs --force,
which is for content that changed without a version bump.
The package version stops matching the library
Keeping the two equal is a convention. doctor checks the manifest and package.json
against each other; nothing checks either against the library. So @acme/docspack@1.4.1 may
document acme@1.4.0, and the manifest is where it says so:
"docspack": { "documents": ["acme@1.4.0"] }Every answer then carries the mapping instead of leaving it to be inferred:
Source: @acme/docspack@1.4.1 — chunks/api-auth.md · documents acme@1.4.0Declare documents before the first correction rather than after. A package that relied on
the versions matching has nothing to state once they stop matching.
Corrections collide with the next release
The generated workflow publishes under the release tag, so a docs-only 1.4.1 takes the
version acme@1.4.1 will want. That release then fails to publish, because npm rejects a
version that already exists. Pick one of two schemes before it happens.
| Scheme | The version means | Suits |
|---|---|---|
| Independent versions | The documentation’s own release | Docs fixed between library releases |
Calendar — 2026.4.1 | When the documentation was cut | Frequent fixes, on any cadence |
A calendar version can never collide with a semver release tag, which is the whole of its
advantage here. Both schemes need documents.
Publish a docs-only version with workflow_dispatch, which takes a version as input. The
release trigger has no tag to give it.
Writing the manifest by hand
build is a convenience, not a requirement. Any package with a valid .llms/manifest.json
can be indexed:
{
"$schema": "https://docspack.dev/schema/v1.json",
"name": "@acme/docspack",
"version": "1.4.0",
"chunks": [
{
"id": "api-auth",
"file": "chunks/api-auth.md",
"tokens": 150,
"tags": ["authentication", "bearer", "api key"],
"entities": ["Client.setApiKey"]
}
]
}id must be unique within the package. file is relative to .llms/. tokens may be
omitted and is estimated at index time — omit it rather than writing 0, which is refused.
tags and entities are optional, and are the cheapest way to make retrieval noticeably
better. documents belongs at the top level, beside name and version.
The whole format — naming, layout, every field, and the rules a consumer enforces when
reading a manifest it did not write — is specified at
docspack.dev/spec, and the JSON Schema is served at the URL
$schema points to.