# One source, two audiences

Documentation has two readers with incompatible needs. A person wants a page: navigation,
prose, a table of contents. An agent wants a passage: small, self-contained, and matched to a
question. Most projects serve them from two pipelines, and the two drift.

This page is the worked example. It is rendered as HTML at
<https://docspack.dev/docs/one-source-two-audiences>, served as Markdown at the same path with
`.md` appended, and shipped as a chunk inside `@docspack/docspack` — all from this one file.

## The two halves

`docspack` is the agent's half. It indexes documentation packages into a local SQLite database
and answers from the versions a project actually installed. Nothing it does requires a website
to exist.

**Sheaf** is the reader's half. It turns a directory of Markdown into a *content graph* — an
ordered, serializable object with a slug, title, summary and headings for every document — and
that graph renders the site. It is four packages, and you take only the ones you need.

<!-- docspack: documents=@docspack/sheaf -->
<!-- docspack: tags=sheaf, content graph, docs site, static site -->

| Package | What it does |
| --- | --- |
| `@docspack/sheaf` | The content graph. No dependencies, no rendering. |
| `@docspack/sheaf-astro` | Renders the graph with Astro's own Markdown pipeline. |
| `@docspack/sheaf-react` | The docs shell: sidebar, page, contents, pager. |
| `@docspack/sheaf-emit` | Turns the graph into a docspack package. |

The graph is the contract. Everything else reads it, and nothing reads anything else.

## Why one parse rather than two

<!-- docspack: documents=@docspack/sheaf -->
<!-- docspack: tags=drift, single source, slugs, ordering -->

A folder of Markdown does not say which documents are published, in what order, or under what
URLs. Two tools reading that folder each decide for themselves, and their answers diverge the
first time a page is renamed or a draft is added. The site drops a page; the index keeps it.

A graph decides once. Drafts, ordering, slugs and headings are settled before either consumer
sees them, so the site and the index cannot disagree about which documents exist or what they
are called.

## What a chunk cites

`docspack build` reads a folder, so the most it can record about a chunk's origin is the file
it came from:

```text
<!-- docspack: from 06-verify.md -->
```

That is no use to whoever reads the answer. A graph knows where its pages are published, so a
chunk emitted from one cites the page and the section:

```text
<!-- docspack: from https://docspack.dev/docs/verify#what-it-reads-and-what-it-never-runs -->
```

The answer an agent gives now carries a link a person can open. This package is built that
way: all of its chunks cite a URL, and every one of those URLs is a route this site serves.

## Building this package from the graph

<!-- docspack: documents=@docspack/sheaf-emit -->

The whole build is one file. It reads the directory, emits the payload, and says nothing else:

```js
import { buildGraph } from "@docspack/sheaf";
import { emitDocspackPackage } from "@docspack/sheaf-emit";

const graph = await buildGraph("./docs");

await emitDocspackPackage(graph, {
  out: ".",
  name: "@docspack/docspack",
  version: "1.0.0",
  documents: ["docspack"],
  baseUrl: "https://docspack.dev/docs",
});
```

`baseUrl` is what turns a filename into a citation. Without it the emitter falls back to the
source path, which is what a folder-based build would have written anyway.

Sections split at every `##`. A section over roughly 800 tokens splits again at its own `###`
headings, because an oversized chunk crowds the rest of an answer out of the response budget —
the same escalation `docspack build` performs, and the reason `docspack doctor` warns above
1500.

## Rendering the same graph as a site

<!-- docspack: documents=@docspack/sheaf-astro -->

Rendering belongs to the host, not to Sheaf. `@docspack/sheaf-astro` binds the graph to Astro's
own Markdown processor — the same one that renders a `.md` file in `src/pages` — so a project's
Markdown configuration keeps applying:

```js
import { createPageRenderer } from "@docspack/sheaf-astro";

const renderPage = createPageRenderer({ syntaxHighlight: false });
const { html, headings } = await renderPage(page);
```

The graph deliberately carries no HTML. That is what lets a React app, a Preact app and a
static Astro page each use their own renderer, and what lets the emitter above ship Markdown
rather than markup.

## Blocks, where the two renderings differ

The list of packages above is a Markdown table. A reader sees it drawn; an agent is given the
table. Neither rendering invents anything the other does not have, because the source wraps the
table in a block rather than replacing it:

```text
:::packages
| Package | What it does |
| --- | --- |
| `@docspack/sheaf` | The content graph. No dependencies, no rendering. |
:::
```

<!-- docspack: tags=blocks, directives, components, presentation -->

`toAgentMarkdown` removes the block and keeps what it wrapped, and every agent-facing output
goes through it: the chunks in this package, the `.md` route for this page, and `llms-full.txt`.
The site's renderer takes the same parse and replaces the block with a component. The rule is
structural, so the emitter needs to know no component names:

**A block wraps content; it never replaces it.** A block with nothing inside it is refused by
file and line, because there would be nothing to give an agent where the page shows something —
and a component the site has no renderer for fails the build for the same reason from the other
side. A block that reached either audience as an absence is the one outcome neither half allows.

## Anchors are the seam

<!-- docspack: tags=anchors, table of contents, heading ids, slugs -->

A table of contents is built from the graph's heading slugs. The `id` attributes it links to
are stamped by the renderer. Different code produces them, and a mismatch fails silently: a
fragment that matches nothing scrolls to the top of the page instead of raising an error.

It is worth guarding deliberately, because nothing reports it. The graph's slugs are checked
against `github-slugger` and against the real Astro processor; the rendered page is checked for
the ids the graph predicted; and the URLs this package publishes are checked against the routes
the site builds.

## Which half do you need

You can take either half alone.

- **docspack only** — you consume other people's documentation packages, or you publish one
  from a folder with `docspack build`. No site, no graph, no Sheaf.
- **Sheaf only** — you want a documentation site inside an app you already have, and you do
  not publish anything for agents. Take the graph and a renderer.
- **Both** — one directory of Markdown becomes the site people read and the package their
  agents answer from, and the two cannot describe different documentation.
