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: 6 additions & 3 deletions .claude/commands/audit-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@ pre-PR pass.
(the highest-confidence drift). Judge each hit — a legitimate "renamed from
X" migration note is fine; a live reference to a dead symbol is not.
4. **Check cross-links.** Every relative markdown link and `github.com/.../blob`
link must resolve. Remember `docs/` is served as a GitHub Pages site under
`baseurl: /xray` — links that escape the `docs/` root must use the full blob
URL, not `../`.
link must resolve. `docs/` is built into a static site by VitePress
(`docs/.vitepress/config.mts`, `base: '/xray/'`) and deployed to GitHub Pages by
`.github/workflows/docs.yml`. In-docs `./*.md` links are fine — VitePress
rewrites them and they also resolve on GitHub. Links that escape `docs/` (to
source files or other repo paths) must use the full `github.com/.../blob` URL,
not `../`, since those resolve on GitHub but not on the built site.
5. **Honesty pass** ([`honesty.md`](../rules/honesty.md)). Flag any claim that
says *more* than the code guarantees, not just outright-wrong ones. Over-claims
are the subtle drift.
Expand Down
2 changes: 2 additions & 0 deletions .claude/rules/docs-freshness.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Coarse on purpose (directory / file → doc), so it survives refactors that a li
| `src/server/env/env.ts` (`XRAY_*` env vars + defaults) | `docs/sdk-python.md`, `docs/architecture.md`, `README.md`, `CONTRIBUTING.md` |
| Control-plane routes (`src/server/**/<slice>.router.ts`) | `docs/architecture.md` (control-plane list). The OpenAPI at `/docs` self-syncs from `describeRoute` — only the *narrative* needs a human. |

> **The public docs site.** `docs/*.md` are the source for the site at https://xray-eval.github.io/xray/ — built by VitePress (`docs/.vitepress/config.mts`, `base: '/xray/'`) and deployed by `.github/workflows/docs.yml` on a release tag. They render on GitHub too, so keep them **plain markdown** (no MDX / Vue components) and keep in-docs links relative (`./other-page.md`).

## 3 · Honesty bar (cross-link)

A doc must not claim more than the code guarantees — see [`honesty.md`](./honesty.md). "The SDK synthesizes TTS" when TTS moved server-side, or "X is supported" when the code does less, is an honesty failure, not a typo. The `/audit-docs` method is built on this: every doc claim is verified *against source*, and an unverifiable claim is removed or softened, never left to look authoritative.
Expand Down
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,8 @@ COMPOSE_PROJECT_NAME=xray
# (non-numeric, empty string) produces a confusing Docker error at `up` time
# rather than a `.env`-level validation, so be deliberate when editing.
# HOST_PORT=8080

# Host-side port that compose.dev.yaml maps to the docs (VitePress) dev server's
# :5173. Browse the live docs at http://localhost:${DOCS_PORT}/xray/. Override
# per worktree to run parallel docs servers alongside HOST_PORT.
# DOCS_PORT=5173
72 changes: 72 additions & 0 deletions .github/workflows/docs-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Docs PR build-gate — compile-only check that the VitePress site still builds,
# so a broken docs build can't merge. No deploy and no Pages permissions: the
# deploy half lives in docs.yml (release tag / workflow_dispatch), kept separate
# so the deploy job never shows up as a skipped check on pull requests.
#
# Local equivalent: `pnpm docs:build`.

name: Docs check

on:
pull_request:
paths:
- "docs/**"
- ".github/workflows/docs-check.yml"
- "pnpm-lock.yaml"
- "package.json"

permissions: {}

concurrency:
group: docs-check-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: vitepress build
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Harden runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
with:
egress-policy: audit

- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Setup Node (pinned via .nvmrc)
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"

# Corepack must run AFTER setup-node — current pnpm (11.x) needs Node ≥22.13,
# newer than the runner's pre-installed Node. package.json#packageManager is
# the source of truth for the pnpm version corepack activates.
- name: Activate the pinned pnpm version via corepack
run: |
corepack enable
corepack prepare --activate
pnpm --version

- name: Resolve pnpm store path
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

- name: Cache pnpm store
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-pnpm-store-

- name: Install (frozen lockfile)
run: pnpm install --frozen-lockfile

# Base path (/xray/) is hardcoded in docs/.vitepress/config.mts.
- name: Build docs
run: pnpm docs:build
103 changes: 103 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Docs site — build the VitePress docs and deploy to GitHub Pages.
# Replaces the old GitHub-managed Jekyll build (docs/_config.yml, removed).
#
# Deploy happens on a release tag (v*.*.*), mirroring publish.yml — docs are cut
# alongside the Docker image. workflow_dispatch is a deliberate escape hatch —
# re-publish (or first-publish, before the next tag) without cutting a release;
# this is the documented reason for the trigger per .claude/rules/supply-chain.md §2.
#
# The PR build-gate (compile-only, no deploy) lives in docs-check.yml — kept in a
# separate file so this deploy job never shows up as a skipped check on PRs.
#
# One-time setup: repo Settings → Pages → Source = "GitHub Actions".
# Local equivalent: `pnpm docs:build` (and `pnpm docs:dev` for a live preview).

name: Docs

on:
push:
tags:
- "v*.*.*"
workflow_dispatch:

permissions: {}

concurrency:
group: docs-${{ github.ref }}
cancel-in-progress: false # never cancel an in-flight Pages deploy (mirrors publish.yml)

jobs:
build:
name: vitepress build
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Harden runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
with:
egress-policy: audit

- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Setup Node (pinned via .nvmrc)
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"

# Corepack must run AFTER setup-node — current pnpm (11.x) needs Node ≥22.13,
# newer than the runner's pre-installed Node. package.json#packageManager is
# the source of truth for the pnpm version corepack activates.
- name: Activate the pinned pnpm version via corepack
run: |
corepack enable
corepack prepare --activate
pnpm --version

- name: Resolve pnpm store path
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

- name: Cache pnpm store
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-pnpm-store-

- name: Install (frozen lockfile)
run: pnpm install --frozen-lockfile

# Base path (/xray/) is hardcoded in docs/.vitepress/config.mts.
- name: Build docs
run: pnpm docs:build

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: docs/.vitepress/dist

deploy:
name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
pages: write # deploy to the Pages service
id-token: write # OIDC token the deploy-pages action verifies
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Harden runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
with:
egress-policy: audit

- name: Deploy
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
21 changes: 21 additions & 0 deletions .github/workflows/supply-chain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ jobs:
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: moderate
# Dev-only docs-toolchain advisories — the same "NOT AFFECTED"
# dispositions documented in pnpm-workspace.yaml's
# `auditConfig.ignoreGhsas`, mirrored here because
# dependency-review-action keeps its OWN allowlist and does not read
# pnpm's auditConfig. All five ride in via vitepress/vite/esbuild/
# mermaid (devDependencies, dropped by the prod image's `--prod`
# install) and the deployed docs are static HTML, so none has a
# production surface. See pnpm-workspace.yaml for the per-advisory
# rationale. Added 2026-06-21.
allow-ghsas: >-
GHSA-fx2h-pf6j-xcff, GHSA-4w7w-66w2-5vf9, GHSA-v6wh-96g9-6wx3,
GHSA-67mh-4wv8-2f99, GHSA-cmwh-pvxp-8882
# ELv2-redistributable bundle: reject every copyleft family on any
# new prod dep. The SPA is built with Bun's HTML bundler, which
# inlines JS into single output chunks — so even weak-copyleft
Expand All @@ -128,4 +140,13 @@ jobs:
EUPL-1.0, EUPL-1.1, EUPL-1.2,
CC-BY-SA-1.0, CC-BY-SA-2.0, CC-BY-SA-2.5,
CC-BY-SA-3.0, CC-BY-SA-4.0
# dompurify is dual-licensed `MPL-2.0 OR Apache-2.0`; the deny-list
# above matches the MPL-2.0 term, but the OR lets us take it under
# permissive Apache-2.0, so no copyleft obligation attaches. It is also
# a dev-only docs dep (mermaid → dompurify), dropped by the prod image's
# `--prod` install and never inlined into the shipped SPA bundle — so
# the bundle source-availability concern the deny-list guards cannot
# arise here. Excluded from license checking by purl (version-agnostic,
# so the planned 3.4.11 bump doesn't reopen it). Added 2026-06-21.
allow-dependencies-licenses: pkg:npm/dompurify
comment-summary-in-pr: on-failure
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ dist/
# Build caches
*.tsbuildinfo

# VitePress docs cache (build output under docs/.vitepress/dist/ is covered by dist/ above)
docs/.vitepress/cache/

# Test & coverage
coverage/
.nyc_output/
Expand Down
31 changes: 31 additions & 0 deletions compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,37 @@ services:
retries: 24
start_period: 60s

# Docs site (VitePress) — live preview at http://localhost:${DOCS_PORT:-5173}/xray/
# while you edit docs/*.md (Vite HMR). Reuses the app's dev image (node+bun+pnpm)
# and the shared node_modules volume; waits for `app` to finish `pnpm install`
# (so it doesn't race on that volume), then serves docs/ over the Vite dev
# server. Dev-only — never built into the shipped image.
docs:
build:
context: .
dockerfile: Dockerfile.dev.server
args:
UID: "${UID:-1000}"
GID: "${GID:-1000}"
container_name: ${COMPOSE_PROJECT_NAME:-xray}-docs
user: "${UID:-1000}:${GID:-1000}"
working_dir: /app
environment:
- HOME=/tmp
- PNPM_HOME=/tmp/pnpm
depends_on:
app:
condition: service_healthy
# `app` owns dependency install; by the time this starts, vitepress and its
# platform esbuild binary are already in the shared volume, so just run the
# server. `--host` exposes it outside the container; base path is /xray/.
command: sh -c "pnpm exec vitepress dev docs --host 0.0.0.0 --port 5173"
volumes:
- ./:/app
- dev_node_modules:/app/node_modules
ports:
- "${DOCS_PORT:-5173}:5173"

volumes:
dev_node_modules:
dev_data:
70 changes: 70 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { defineConfig } from "vitepress";
import { withMermaid } from "vitepress-plugin-mermaid";

// Site config for the public docs at https://xray-eval.github.io/xray/.
// Built by `pnpm docs:build`, deployed by .github/workflows/docs.yml on release.
// `withMermaid` registers client-side mermaid rendering for the ```mermaid fences
// (no headless browser at build time); GitHub renders the same fences natively.
export default withMermaid(
defineConfig({
title: "xray",
description:
"Open-source, self-hosted replay/eval framework for LiveKit voice agents",
base: "/xray/", // GitHub Pages project site
cleanUrls: true,
lastUpdated: true,
appearance: "dark", // dark-first (matches the app palette); the light toggle still works
// localhost URLs (the quickstart's "open http://localhost:8080") are
// intentionally unreachable at build time; everything else stays checked.
ignoreDeadLinks: [/^https?:\/\/localhost(:\d+)?/],
// Mermaid: render at natural (readable) size instead of shrinking to fit the
// prose column — wide diagrams scroll inside their panel (see style.css). The
// plugin still auto-switches to mermaid's dark theme with the site. Defaults
// here replace the plugin's, so keep securityLevel/startOnLoad explicit.
mermaid: {
securityLevel: "loose",
startOnLoad: false,
// Render labels as native SVG <text>, not HTML <foreignObject>. The HTML
// labels measure their width against .vp-doc's font, so cloning the SVG into
// the fullscreen viewer (a different CSS context) reflowed and clipped them.
// SVG text bakes geometry into the SVG, so the clone renders identically.
htmlLabels: false,
flowchart: { useMaxWidth: false, htmlLabels: false },
sequence: { useMaxWidth: false },
er: { useMaxWidth: false },
class: { useMaxWidth: false, htmlLabels: false },
},
themeConfig: {
search: { provider: "local" }, // built-in MiniSearch — no native binary, no external service
// Reading order runs from "use it" to "understand it": Architecture (the
// deep mental model) sits last, after the practical guides.
nav: [
{ text: "Quick start", link: "/quickstart" },
{ text: "Integrate", link: "/integrate" },
{ text: "Python SDK", link: "/sdk-python" },
{ text: "Wire contract", link: "/wire-contract" },
{ text: "Architecture", link: "/architecture" },
],
sidebar: [
{
text: "Documentation",
items: [
{ text: "Overview", link: "/" },
{ text: "Quick start", link: "/quickstart" },
{ text: "Integrate", link: "/integrate" },
{ text: "Python SDK", link: "/sdk-python" },
{ text: "Wire contract", link: "/wire-contract" },
{ text: "Architecture", link: "/architecture" },
],
},
],
socialLinks: [
{ icon: "github", link: "https://github.com/xray-eval/xray" },
],
editLink: {
pattern: "https://github.com/xray-eval/xray/edit/main/docs/:path",
text: "Edit this page on GitHub",
},
},
}),
);
Loading
Loading