Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,19 @@ prints advisory observations (which never fail the build) when a project's decla
pin a project's live and prerelease documentation to refs (`default`, `latest-stable`,
`latest-prerelease`, or an explicit branch/tag). The default channel owns `/docs/<id>/`, the
other is served at `/docs/<id>/<channel>/`, and `docsPage` resolves against the default channel.
Each channel is a first-class docs set — its own sidebar topic (`<Project> <version>`), its own
`/projects/<id>/<channel>/llms*.txt` bundle, and its own entry on the docs portal — because the
same product at another version is different documentation.
Only `github-path` supports two channels; `wiki`/`readme` have a single version. Promotion is
automatic: the prerelease channel is withheld once a stable release (GitHub release or NuGet
version) catches up with the prerelease line, and returns with the next prerelease — never
delete the declaration to stop publishing it.
- Agent Skills a project publishes are declared in `skills:` (ADR 0010) at
`skills/<name>/SKILL.md`; the sync mirrors the whole directory to
`/skills/<id>/<name>/` and every page renders the same install block
(`npx skills add <owner>/<repo>`). Never ship a skill in a package or copy it
`/skills/<id>/<name>/` and each project page renders the same install block
(npm, bun, pnpm and yarn runners) plus that project's skill list with the
descriptions from each `SKILL.md`. `/skills/` redirects to `/projects/`.
Never ship a skill in a package or copy it
into an agent directory at build time, and never hand-edit `src/public/skills/`.
- `order` values are unique and control the catalogue order.
- `related`, `supersedes`, `supersededBy` must reference known ids.
Expand Down
47 changes: 32 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,12 @@ docs:
```

The `live` channel is the default and keeps the canonical `/docs/<project>/`
URLs; the `prerelease` channel is served at `/docs/<project>/prerelease/`. The
sidebar groups pages per channel, and every page offers a switcher. Only
`github-path` sources support two channels.
URLs; the `prerelease` channel is served at `/docs/<project>/prerelease/`.
Versions are first-class: each published channel is its own sidebar topic
(`ZodSharp 2.4.2`, `ZodSharp 3.0.0-prerelease.2`) with its own page list, its
own machine-readable bundle, and its own entry on the documentation portal —
same product, different version, therefore different docs. Every page also
offers a switcher. Only `github-path` sources support two channels.

**Promotion is automatic.** The build publishes the prerelease channel only
while a prerelease version outranks the newest stable release — comparing GitHub
Expand All @@ -333,18 +336,23 @@ config to declare).
## Agent skills

Purview projects publish Agent Skills at `skills/<name>/SKILL.md` and deliver
them with the skills CLI (`npx skills add <owner>/<repo>`); nothing ships a
skill in a NuGet package. The catalogue declares the skills a project authors,
`just data-sync` mirrors each whole skill directory to `public/skills/<project>/
<skill>/…`, and the site presents them two ways (ADR 0010):

- `/skills/` lists every skill grouped by project, with the install instructions
and a direct link to each `SKILL.md`;
- `/skills.json` is the machine-readable index (install commands, URLs, files).
them with the skills CLI (`npx`, `bunx`, `pnpm dlx` or `yarn dlx` — every runner
is offered); nothing ships a skill in a NuGet package. The catalogue declares the
skills a project authors, `just data-sync` mirrors each whole skill directory to
`public/skills/<project>/<skill>/…`, and the site presents them in project
context (ADR 0010):

- each project page lists that project's skills with the description from its
own `SKILL.md` and a direct link to the file;
- `/skills.json` is the machine-readable index (install commands per package
manager, URLs, files);
- `/skills/` redirects to `/projects/`, because a skill only makes sense next to
the project that authors it.

The install block is one component (`components/SkillInstall.astro`), rendered
unchanged on `/skills/` and on every project page that declares skills, so the
instructions are identical across the suite:
unchanged on every project page that declares skills, so the instructions are
identical across the suite. Each command is an Expressive Code block, so it
carries the copy button:

```yaml
skills:
Expand Down Expand Up @@ -379,6 +387,10 @@ build-time.
- Without a cache, the site falls back to committed fixtures under
`src/fixtures/releases/` and labels the data source (`live` / `cache` /
`fixture`) in the UI.
- A package that has only prereleases is still listed: the stable cell reads `—`
and the prerelease cell carries the version. Package membership comes from the
catalogue (`packages:`), so a new package is one manifest entry — the release
data supplies its versions, downloads and deprecation state.
- `just refresh-fixtures` regenerates the committed test fixtures from live
sources (review the diff before committing).

Expand All @@ -392,8 +404,13 @@ Each documented project is also emitted as its own scoped bundle by the
discovery integration (see [docs/discovery.md](docs/discovery.md)) at
`/projects/<id>/llms.txt` (a concise index) and `/projects/<id>/llms-full.txt`
(the project's aggregated documentation), following the `llms.txt` convention at
the per-project scope. The project page, the project's documentation overview
page, the documentation portal and `/llms.txt` all link to those paths.
the per-project scope. Documentation versions are first-class, so a second
published channel gets its own pair under its channel segment
(`/projects/<id>/<channel>/llms.txt`, `/projects/<id>/<channel>/llms-full.txt`):
one corpus never mixes two versions of the same page. Every bundle names its
version and links the other versions' bundles. The project page, each
documentation version's overview page, the documentation portal, `discover.json`
and `/llms.txt` all link to those paths.

The suite-level narrative in `/llms.txt` is generated from the project catalogue
(`src/src/lib/llms-details.ts`) rather than authored by hand, so it cannot drift
Expand Down
25 changes: 17 additions & 8 deletions docs/decisions/0009-documentation-channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,20 @@ history has to survive between builds, and an existing page is never overwritten
while two channels are published: promotion withholds the prerelease channel,
so the switcher disappears with it and nothing in the UI advertises a version
that is no longer published (its retired URLs redirect instead).
- The sidebar shows one Starlight group per channel with the same labels
(`Live 2.4.2`, `Prerelease 3.0.0-prerelease.2`); single-channel projects keep
the flat list.
- The per-project `llms-full.txt` corpus publishes the live channel only, and
the concise `llms.txt` links the prerelease channel when it exists. Two
versions of one page in a single corpus is ambiguous to a model.
- The sidebar treats each channel as a **topic of its own**: `ZodSharp 2.4.2` and
`ZodSharp 3.0.0-prerelease.2` are separate entries in the topic picker, each
with a flat page list, exactly as two projects would be. A channel is a whole
documentation set, not a section of one, so grouping both under a single
project topic would claim one tree while the pages belong to two. A
single-channel project keeps the bare project name.
- Every published version has its own machine-readable bundle. The default
channel keeps `/projects/<id>/llms.txt` and `llms-full.txt`; any other channel
mirrors its documentation URL shape at
`/projects/<id>/<channel>/llms.txt` and `llms-full.txt`. Each bundle carries a
version block (version, channel, source ref) and links the other versions'
bundles, and never mixes two versions of one page in a single corpus.
`discover.json` describes each version under `docsChannels`, and the sitemaps
list every version's bundles.
- `wiki` and `readme` sources support exactly one channel. A wiki is a
standalone git repository with a single HEAD, and a README is one page; the
schema rejects `channels` on them instead of accepting a declaration it cannot
Expand All @@ -177,5 +185,6 @@ history has to survive between builds, and an existing page is never overwritten
search engines and from `llms.txt`, and make `/docs/<id>/<page>/` ambiguous.
- **Versioning the wiki source.** A repository wiki has no tags, so there is
nothing to pin. Documented as unsupported rather than half-implemented.
- **Keeping both channels in `llms-full.txt`.** Rejected: one corpus should not
contain two versions of the same page.
- **Keeping both channels in one `llms-full.txt`.** Rejected: one corpus should
not contain two versions of the same page. Each channel publishes its own
`/projects/<id>[/<channel>]/llms*.txt` bundle instead.
31 changes: 20 additions & 11 deletions docs/decisions/0010-agent-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,34 @@ nothing on another) and could not read a skill without cloning its repository.
to `public/skills/<project>/<skill>/…` and caches it under `.cache/skills/`,
so `https://purview.dev/skills/<project>/<skill>/SKILL.md` (and its
references) is a real, stable URL. `public/skills/` is generated and
gitignored like `public/schemas/`.
gitignored like `public/schemas/`. A skill's description is read from its own
YAML front matter — inline or a folded block scalar (`description: >-`) — so
what the site shows is what the CLI shows.
3. **One shared instruction block, everywhere.** A single component
(`components/SkillInstall.astro`) renders the command, the `#<ref>` pinning
syntax and the `skills-lock.json` restore note. It is used unchanged on
`/skills/`, on each project page that declares skills, and in
`/skills.json` — so the instructions cannot drift between projects.
4. **`/skills/` is the browse and install page**, grouped by project, and
`/skills.json` is the machine-readable index (name, description, directory,
entry URL, file list, install commands).
(`components/SkillInstall.astro`) renders the install commands, the `#<ref>`
pinning syntax and the `skills-lock.json` restore note. Every command is an
Expressive Code block (so it carries the copy button), and every package
manager a reader may use is offered — `npx`, `bunx`, `pnpm dlx`, `yarn dlx` —
rather than telling a bun or pnpm user to reach for npm. The component is used
unchanged on each project page that declares skills and in `/skills.json`, so
the instructions cannot drift.
4. **Skills are presented in project context.** Each project page lists the
skills that project authors, with the description from each `SKILL.md` and a
direct link to the file; the catalogue cards show how many skills a project
has. `/skills.json` remains the machine-readable index (name, description,
directory, entry URL, file list, install commands per package manager), and
`/skills/` redirects to `/projects/` — a site-wide list separated a skill from
the project that gives it meaning.
5. **Guards, not hopes.** `just check-projects` fails when a declared skill has
no mirror, `just check-generated` fails when a declared skill is missing from
the built site, and the sync fails when a declared skill resolves to no files
at its repository path.

## Consequences

- `pm-skills` gains fourteen declarations; `aspire-resourcekit` and `results` gain
five each, so the same install block now appears on their project pages and on
`/skills/` with their own repositories.
- `pm-skills` gains its own declarations; `aspire-resourcekit` and `results` gain
five each, so the same install block now appears on each of their project pages
with its own repository.
- The site never claims to *host* the CLI source: it serves the skill files. The
CLI still installs from the git repository, which is what records a pinnable
ref; the portal URLs exist so a skill can be read, reviewed or fetched without
Expand Down
12 changes: 12 additions & 0 deletions docs/discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,21 @@ release cache). **There is no hand-maintained list of URLs.**
| Site map (JSON) | `/discover.json` | integration (`lib/discovery/manifest.ts`) |
| Root LLM content | `/llms.txt`, `/llms-small.txt`, `/llms-full.txt` | `starlight-llms-txt` |
| Per-project LLM content | `/projects/<slug>/llms.txt`, `/projects/<slug>/llms-full.txt` | integration (`lib/discovery/project-llms.ts`) |
| Per-version LLM content | `/projects/<slug>/<channel>/llms.txt`, `/projects/<slug>/<channel>/llms-full.txt` | integration (`lib/discovery/project-llms.ts`) |
| IndexNow key file | `/<INDEXNOW_KEY>.txt` (written from the secret) | deploy workflow via `bun run discovery:key` |
| Internal deployment state | `src/.cache/discovery/state.json` (never published) | integration |

The integration lives in `src/config/discovery-integration.ts`; the shared model
is in `src/src/lib/discovery/`.

A project that publishes more than one documentation version (ADR 0009) gets one
bundle pair **per version**. The default channel keeps the historical
`/projects/<slug>/llms*.txt` paths; every other channel adds its id as a path
segment, mirroring its `/docs/<slug>/<channel>/` URLs. Each bundle names the
version it documents and links the other versions' bundles, and no corpus
contains two versions of the same page. `discover.json` exposes every version
under `docsChannels` (schemaVersion 2).

### Sitemap partitioning

- **pages.xml** — hand-authored pages (`/`, `/about/`, `/docs/`, `/projects/`,
Expand Down Expand Up @@ -181,6 +190,7 @@ Automated (nothing to do per deploy):
- [x] `robots.txt` generated and references the sitemap
- [x] Root `llms.txt` / `llms-full.txt` generated
- [x] Per-project `llms.txt` / `llms-full.txt` generated
- [x] Per-version `llms.txt` / `llms-full.txt` generated for every published channel
- [x] `discover.json` generated
- [x] IndexNow key file written from the secret by the deploy workflow
- [x] IndexNow notifies changed URLs after a successful deploy
Expand All @@ -203,6 +213,8 @@ One-time (operator):
- [ ] Verify the first IndexNow deployment
- [ ] Verify a representative project LLM resource
(`https://purview.dev/projects/zodsharp/llms.txt`)
- [ ] Verify a representative versioned LLM resource while a second channel is
published (`https://purview.dev/projects/zodsharp/prerelease/llms.txt`)

## Security

Expand Down
54 changes: 39 additions & 15 deletions src/astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import starlightSidebarTopics from 'starlight-sidebar-topics';
import discoveryIntegration from './config/discovery-integration';
import docsRedirectsIntegration from './config/docs-redirects-integration';
import { remarkMermaid } from './config/remark-mermaid.mjs';
import { projectLlmsUrl } from './src/lib/discovery/urls';
import { projectChannelLlmsUrl, projectLlmsUrl } from './src/lib/discovery/urls';
import { readDocsManifest } from './src/lib/docs/aggregate';
import { buildProjectItems } from './src/lib/docs/sidebar';
import { channelDisplayLabel, channelTopicLabel } from './src/lib/docs/channels';
import { buildProjectTopics } from './src/lib/docs/sidebar';
import { buildLlmsDetails } from './src/lib/llms-details';
import { loadProjects } from './src/lib/manifest/load';
import { SITE, BRAND } from './src/lib/site';
Expand Down Expand Up @@ -53,12 +54,18 @@ const topicBadges: Partial<Record<DisplayStatus, { text: string; variant: 'cauti
};
const sidebarTopics = [
{ label: 'Documentation home', link: '/docs/' },
...docsProjects.map((project) => ({
label: project.name,
link: `/docs/${project.id}/`,
badge: topicBadges[displayStatus(project)],
items: buildProjectItems(project, docsManifest),
})),
// Each published documentation version is its own topic (ADR 0009): the same
// product at a different version is a different documentation set, with its
// own pages, its own sidebar and its own machine-readable bundle. A project
// with one channel therefore produces exactly the topic it always did.
...docsProjects.flatMap((project) =>
buildProjectTopics(project, docsManifest).map((topic) => ({
label: topic.label,
link: topic.link,
badge: topicBadges[displayStatus(project)],
items: topic.items,
})),
),
];

const HEAD = [
Expand Down Expand Up @@ -215,13 +222,30 @@ export default defineConfig({
url: absoluteUrl('/releases/'),
description: 'GitHub and NuGet release information.',
},
// Per-project machine-readable bundles, emitted by the discovery
// integration at /projects/<id>/llms.txt.
...docsProjects.map((project) => ({
label: project.name,
url: projectLlmsUrl(project.id),
description: project.shortDescription,
})),
// Per-version machine-readable bundles, emitted by the discovery
// integration at /projects/<id>[/<channel>]/llms.txt. Each published
// documentation version advertises its own bundle, so a model can
// read the version it needs instead of one corpus with two versions
// of the same page.
...docsProjects.flatMap((project) => {
const channels =
docsManifest?.projects.find((entry) => entry.projectId === project.id)?.channels ??
[];
if (channels.length === 0) {
return [
{
label: project.name,
url: projectLlmsUrl(project.id),
description: project.shortDescription,
},
];
}
return channels.map((channel) => ({
label: channelTopicLabel(project.name, channel, channels.length),
url: projectChannelLlmsUrl(project.id, channel.id, channel.default),
description: `${project.shortDescription} (${channelDisplayLabel(channel)})`,
}));
}),
],
promote: ['index*', 'docs/*/index'],
demote: [],
Expand Down
Loading
Loading