docspack v1.2.0
Documentation

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:

bash
npx docspack init

It 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:

bash
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

text
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.yml

Two 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:

json
"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:

json
"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:

text
## @acme/docspack@2026.1.0/button-props
Source: @acme/docspack@2026.1.0 — chunks/button-props.md · documents @acme/core@0.17.0

Without 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:

bash
npx docspack verify
text
drift @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.md

It 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

bash
npx docspack build --from ./docs --name @acme/docspack --pkg-version 1.4.0

The 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, ExamplesCategory 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:

bash
npx docspack build --from ./reference --min-chunk-tokens 400 --max-chunk-tokens 900

Adjacent 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:

md
## 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

bash
npx docspack build --openapi ./openapi.json

Every 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:

bash
npx docspack sources
npx docspack init --mirror hono --name @docspack-community/hono --yes

This 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

bash
npx docspack doctor

doctor 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 with package.json, or a files array 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

bash
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:

bash
npx docspack eval ./eval/queries.json --min-hit-rate 90

The set is a JSON array of questions and the chunk ids that would answer them:

json
[
  { "query": "how do I verify a webhook signature", "expect": "webhooks-signing" },
  { "query": "rate limits", "expect": ["rate-limits", "errors"] }
]
text
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 answer

This 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

bash
npx docspack build --help
npx docspack doctor --help
npx docspack eval --help

The 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

bash
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 again

With 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:

bash
$EDITOR docs/03-authentication.md
npm version patch                  # 1.4.0 becomes 1.4.1
npm publish                        # prepublishOnly rebuilds and checks again

A 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:

json
"docspack": { "documents": ["acme@1.4.0"] }

Every answer then carries the mapping instead of leaving it to be inferred:

text
Source: @acme/docspack@1.4.1 — chunks/api-auth.md · documents acme@1.4.0

Declare 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.

SchemeThe version meansSuits
Independent versionsThe documentation’s own releaseDocs fixed between library releases
Calendar — 2026.4.1When the documentation was cutFrequent 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:

json
{
  "$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.

The same ranking docspack search uses, over the same 13 documents.