docs-system
The public contract and reusable tooling behind the unified beyond10x documentation site. Docs System validates repository-owned publication declarations, safely collects their passive content, builds deterministic discovery and change data, and supplies shared React renderers and GitHub Pages compatibility routes.
Consumer repositories pin an immutable Git commit. This package is private to the Git transport and is not published to npm:
{
"dependencies": {
"@beyond10x/docs-system": "github:beyond10x/docs-system#0123456789abcdef0123456789abcdef01234567"
}
}
Documentation sources
Every participating repository owns a root b10x.docs.yaml. b10x-docs/v4 keeps the v3 data-only
source and central-delivery boundary and adds explicit experience and page contracts. It declares:
- the repository display name and public documentation surfaces;
- canonical route bases, publication state, experience IDs, relationships, and document defaults;
- allowlisted Markdown/MDX, structured data, blog, asset, OpenAPI, and JSON Schema inputs;
- an optional fixed vocabulary of shared components; and
- the Website publisher and
beyond10x.github.iodelivery origin.
b10x-docs/v5 is v4 plus declared navigation and built assets. source.navigation.sidebar is
either autogenerated/flat or a list of categories (label, optional collapsed, items) whose
leaves are document paths relative to source.root; source.navigation.landing names the document
served at the route base; source.navigation.menuWithheld lists routable documents kept out of the
menu; the surface-level builtAssets list names built files the repository's own credential-free
workflow commits. Validation refuses any sidebar leaf, landing or withheld path that
source.documents does not publish (or that lies under a directory the collector never enters),
any non-canonical or repeated path, a built asset that is a glob, a directory or under a directory
the collector never enters, and a withheld document that is also a sidebar leaf, naming the
path; invisible characters in a refused path are shown as \uXXXX. A withheld document may be the
landing document. The public registry carries a v5 surface in
the v4 shape, without the sidebar tree, landing, withheld documents or built assets.
Publication answers whether and where documentation is delivered. Access answers who can use an adoption path or artifact; publishing a page never implies that its required product artifact is public. Versions 1 through 3 remain byte-immutable and readable during migration. Validate any supported contract with:
b10x-docs validate b10x.docs.yaml changes/*.yaml sources.lock.json redirects.yaml
The v3/v4 collector never imports repository code. It refuses path traversal, symlinks, executable MDX, undeclared components, invalid source types, unmatched declarations, duplicate outputs, and duplicate specification routes. Whole-line MDX comment expressions and validated trailing explicit heading ids are accepted as inert Docusaurus syntax; inline or executable expressions remain refused. Validation never rewrites those markers, so collected and copied files retain their original bytes. The collector can validate and hash in place or copy only the declared bytes:
b10x-docs collect --manifest b10x.docs.yaml --repository-root . \
--index-out collection.json --document-index-out documents.json --out .generated/sources
The deterministic index contains every repository-relative source path, staged output path, byte
size, SHA-256 digest, and one aggregate contentSha256. A b10x-sources/v1 lock binds that digest and
the manifest digest to an exact 40-character Git commit. b10x-docs-collection/v1 and
b10x-sources/v1 are unchanged by v4; effective page metadata is emitted in the separate
b10x-doc-index/v1 sidecar.
Normalized producer bundles
b10x-docs-bundle/v1 is the immutable handoff from source CI to the aggregate Website. Its root
contains exactly:
bundle.json
b10x.docs.yaml
collection.json
tree/<repository-relative source path>
tree/ is the union of every sourcePath in b10x-docs-collection/v1 and every regular
changes/**/*.yaml impact record. The sorted bundle.json.files inventory binds each path, byte
size, and SHA-256 digest. contentSha256 is the SHA-256 of that inventory's compact canonical JSON;
manifestSha256 and collectionSha256 bind the two exact input files. The builder also independently
recomputes collection.json.contentSha256, refuses overlaps, traversal, symbolic links, non-regular
files, unsupported change schemas, and changed source bytes, and creates only a new output directory.
The JSON Schema is available at schema/b10x.docs.bundle.v1.schema.json and through the
@beyond10x/docs-system/schema/bundle/v1 package subpath.
Build and independently validate a bundle with the Rust CLI:
node dist/cli.js collect --manifest b10x.docs.yaml --repository-root . \
--index-out .generated/collection.json
cargo run --locked --bin b10x-docs-bundle -- build \
--repository-root . --collection .generated/collection.json \
--out .generated/bundle --commit "$GITHUB_SHA" --producer-run-id "$GITHUB_RUN_ID"
cargo run --locked --bin b10x-docs-bundle -- validate \
--bundle .generated/bundle --commit "$GITHUB_SHA" --producer-run-id "$GITHUB_RUN_ID"
The manifest always records the producer run. artifactId and artifactDigest are an optional pair
for contexts where both already exist; a normal upload learns them only after construction and
records that post-upload identity in Atlas. The credential-free composite action at
.github/actions/bundle/action.yml wraps collection and construction for exact Git-pinned callers.
Producer workflows must trigger for b10x.docs.yaml, every manifest-declared source path, and
changes/**/*.yaml, or impact and release feeds can become stale.
Bundle versions move consumer first. Website and Atlas first accept versions N and N+1, producers then emit N+1, durable published inputs migrate, and support for N is removed only in a later control-plane change. A v1 validator deliberately refuses an unknown version with this ordering in its error rather than guessing compatibility.
Experiences and page metadata
The Website owns one b10x-experiences/v1 catalog. Repository v4 surfaces and pages reference its
stable experience IDs. An experience contains ordered adoption paths. Each path declares support,
its own access requirement, an optional URL, and every required artifact ID. Artifacts independently
declare kind, availability, and access.
Effective path access is the most restrictive of the path and all required artifacts:
public < account-required < approval-required < private
A path is actionable only when support is supported, preview, or experimental, every required
artifact is available, effective access is not private, and the path has a URL. Otherwise the
evaluator returns typed blockers and a human-readable explanation for a status-only presentation.
Immutable v1-v3 adoption actions normalize with unspecified access/support, no artifacts, and
actionable: false; unspecified is never accepted in a v4 declaration.
V4 documentDefaults always declare audiences, experience IDs, support, and access. A page may
replace them by placing a b10x-doc-page/v1 value under the ordinary top-level b10x frontmatter
property; unrelated Docusaurus frontmatter remains untouched:
---
title: Validate a documentation source
b10x:
schema: b10x-doc-page/v1
audiences: [operator]
experienceIds: [validate-documentation]
support: preview
access: account-required
---
Validate embedded page metadata directly with b10x-docs validate-page docs/example.md. Node
orchestration can use readExperienceCatalog, normalizeManifestExperiences,
resolveDocumentPageMetadata, and buildDocumentPageIndex from the Node-safe experiences,
documents, and manifest package subpaths.
Node orchestration imports the dedicated server-safe subpaths. The package root also exports React components and therefore belongs in Docusaurus, not a plain Node collector process:
import {readManifest, readSourceLock} from '@beyond10x/docs-system/manifest';
import {collectManifestSources} from '@beyond10x/docs-system/collector';
import {writeRedirectMap} from '@beyond10x/docs-system/redirects';
Discovery, changes, and compatibility
Generate or check the standard marked README entry block rather than hand-writing cross-ecosystem links:
b10x-docs readme --manifest b10x.docs.yaml --file README.md
b10x-docs readme --manifest b10x.docs.yaml --file README.md --check
b10x-change/v2 adds typed affected repositories, surfaces, components, and APIs. Existing v1 change
documents remain valid. Snapshot generation keeps explicit impact records and unenriched technical
releases in distinct typed channels while retaining a deterministic combined ledger:
b10x-docs snapshot --registry-out ecosystem.json --ledger-out changes.json \
--rss-out changes/feed.xml --json-feed-out changes/feed.json \
--release-facts release-facts.json ../*/b10x.docs.yaml ../*/changes/*.yaml
Generate repository Pages compatibility façades from b10x-redirects/v1:
b10x-docs redirects --map redirects.yaml --out public --alias-root unified-artifact
HTML routes receive deterministic canonical/noindex redirect pages that preserve the query and fragment. RSS, JSON, OpenAPI, schemas, and downloads use byte-preserving static aliases instead of HTML redirects.
Shared components
PageHeaderandSectionHeaderfor consistent page hierarchyCardGrid,ContentCard,ProjectCard, andAdoptionCardFactGrid,SearchField, andFilterChipGroupfor discovery viewsCalloutwith note, success, warning, and danger tones;BoundaryNoticeis its compatibility wrapperCodeExample,CommandExample, andCodeTabsDataCatalogandDependencyGraphfor typed repository-owned dataDiagramfor Mermaid source or generated SVG;DiagramFramefor an existing rendererScrollableTablefor native tables with keyboard access to overflowing columnsOpenApiReferenceandJsonSchemaViewerfor read-only OpenAPI 3.1 documentsStatusBadgeandEcosystemSwitcherEcosystemFamilyGatewayfor a metadata-driven public entry experienceChangeTimelineEntryfor ecosystem impact feeds
Import @beyond10x/docs-system/tokens.css once. Components use stable b10x-* class names; project
identity is data, while Website owns the unified shell. The semantic light/dark tokens meet at least
WCAG AA contrast for normal text and 3:1 for component boundaries. Existing --b10x-ink,
--b10x-text, and related names remain aliases; new integrations should prefer the
--b10x-color-* tokens. Footer shell colors are available as --b10x-footer-*.
Every Docusaurus consumer should also load the shared Prism grammar list. It covers Bash, C/C++, Go, HTTP, Python, Rust, shell transcripts, TOML, YAML, and the other languages present in public repository documentation:
import {PRISM_ADDITIONAL_LANGUAGES} from '@beyond10x/docs-system/code';
export default {
themeConfig: {
prism: {additionalLanguages: [...PRISM_ADDITIONAL_LANGUAGES]},
},
};
Use normalizeMarkdownFenceLanguage when preparing repository-owned Markdown. It makes common
aliases deterministic: sh becomes bash, console becomes shell-session, and yml becomes
yaml. Unknown project-specific grammars are retained instead of silently losing highlighting.
describeMarkdownFenceLanguage adds the canonical visible label and distinguishes commands,
terminal transcripts, plain output, and source. CommandExample always uses shell-session;
CodeExample normalizes the requested language. The shared tokens apply the same labelled code
surface to ordinary Docusaurus Markdown fences; a line wider than the block wraps inside it with a
2ch hanging indent, so nothing is cut at the edge.
OpenApiReference is an embeddable section, never a page landmark. It defaults to an h2 and can sit
under an existing documentation h2 without introducing another h1 or main:
<OpenApiReference document={openapi} sourceUrl={sourceUrl} headingLevel={3} />
Diagrams retain their full canvas inside one bounded, focusable, two-axis keyboard-scrollable
viewport. Diagram centers the initial view after Mermaid or SVG content is measured. A dependency graph
always supplies a node and relationship list; generic diagrams can provide the same complete
alternative with alternative. Set minWidth for a dense graph without adding a second viewport;
use initialPosition="start" only when reading order should begin at the canvas origin:
<DependencyGraph
nodes={nodes}
edges={edges}
title="Public repository relationships"
description="Provider and consumer relationships declared by owning repositories."
minWidth="112rem"
/>
DiagramFrame wraps an existing renderer without calling Mermaid itself. It preserves the child's
accessible SVG description, uses the visual's intrinsic width, and starts at the canvas origin by
default. Nested frames reuse the outer viewport, so a theme can frame ordinary Mermaid fences
without adding another viewport to Diagram or DependencyGraph:
<DiagramFrame title="Request flow" description="A request passes through admission to execution.">
<OriginalMermaid value={source} />
</DiagramFrame>
ScrollableTable accepts native table props and children, including captions and header scopes.
It adds a focusable, named scroll region and visible keyboard instructions only when columns
overflow. Use label to name the region when the surrounding context is insufficient:
<ScrollableTable label="Operation outcomes">
<caption>Operation outcomes</caption>
<thead><tr><th scope="col">Outcome</th><th scope="col">Meaning</th></tr></thead>
<tbody><tr><th scope="row">Refused</th><td>No provider request was sent.</td></tr></tbody>
</ScrollableTable>
The family gateway accepts either the registry or its surface array. It treats an ungrouped
front-door surface as Start, derives membership and item order from
surface.source.navigation.group/order, and takes family order from the shell. Families not named
by the caller follow in lexical order, so membership and taxonomy never live in component code:
<EcosystemFamilyGateway
registry={registry}
current="harness/docs"
familyOrder={['Foundation', 'Build', 'Services', 'Products']}
/>
For non-React preparation code, deriveEcosystemNavigation is available from the Node-safe
@beyond10x/docs-system/navigation subpath. It also reports ungrouped surfaces rather than silently
assigning them to an invented family.
Gate
npm ci --ignore-scripts
npm run gate
Product sites
A product repository (Canon, ELS, Commission, Loom, a2a) can turn its Docusaurus site into a product front page with one wrapping call. Documentation pages keep their Markdown and inherit the same tokens.
import {withProductSite} from '@beyond10x/docs-system/product-site';
export default withProductSite(config, {landing: './product.json', mark: 'C'});
Options: landing, product (canon, els, loom, commission, ess or a2a: the signature hue on
the navbar mark, kicker dot and hero emphasis), mark, favicon (a path under one of the site's static
directories, passed to Docusaurus as favicon; a favicon set in the config wins), fonts (default
on) and admonitionGuard (default on: the build fails when a raw :::kind Title line reaches a page).
Colour roles are reserved and validated per mode: status is an ordinal ramp with ○ ◐ ● glyphs, truth
values carry ✓ ? ✕, protocol kinds keep the graph colours, and the product hue is identity only.
In docs, table cells and inline code that are exactly TRUE, FALSE, UNKNOWN, shipped,
decided or planned become chips, and a protocol kind becomes a chip in a table cell (in prose it
stays code; use <Kind> for a kind chip there). Short inline code with a space or hyphen, such as
cargo install, stays on one line; :::shipped[Title] and admonitions titled
"Shipped …", "Decided …" or "Planned …" take the status tone. Recorded terminal sessions may carry
tones ({"passed:": "true"}); exits show in a gutter. protocol/1 fences get a kind gutter. Sites
using @docusaurus/theme-mermaid get diagrams themed from the tokens and should drop their own
mermaid.theme.
The wrapper adds the product plugin (the shared tokens plus product-components.css and
product-theme.css, a theme path that registers the product MDX components, status admonitions and
the @theme/ProductLandingPage route component, and a not-found page linking to the main doc and
the home page, with a meta description and noindex), the font directory to staticDirectories, a
default 1200 × 630 share card at <baseUrl>b10x-social/card.png as themeConfig.image (a site that
sets themeConfig.image keeps its own), a generated SVG favicon at <baseUrl>b10x-favicon.svg (the mark
in white on the product's ink, ringed with its hue, or on the family green without a product) when
neither the config nor the favicon option names one, an empty .nojekyll at the root of the build
output when the build has none, the
chips pass and status admonition keywords to the docs and pages plugins, Prism themes whose every
token clears 4.5:1 on its code surface with the kind-gutter magic comments, a console Prism
language (a fence of bare commands is highlighted as Bash with a drawn $ prompt that is neither
selected nor copied; a transcript that writes its own prompts is highlighted as shell-session),
and a dark default that
still follows the reader's system setting. The landing page renders at /, so the docs plugin moves to routeBasePath: 'docs'.
product.json is a b10x-product-landing/1 document: the promise (one entry per line, the last one
emphasised), the lede and actions, optional hero art and KPI row, and ordered sections of kind
features, flow, status, protocol-graph, domain-graph, terminal or related. A section
may name a JSON file instead of embedding it; the plugin reads and checks it at build time. Every
field below is optional and additive: a site that sets none of them renders as before.
| Field | Shape | Effect |
|---|---|---|
product.description | a string of at most 160 characters | the home page's meta and og:description; without it the lede, cut at the last whole sentence or word that fits |
product.art | {kind, data or session, caption?}, kind terminal, protocol-graph, domain-graph, case, code-pair | hero aside drawn from that file; replaces product.terminal |
product.kpis | count names or {count, label?}: entities, relations, lifecycles, nodes, actions, evidence, claims, outcomes, obligations, commands, capabilities, shipped, decided, planned | KPI row; graph counts read the art, then the first graph section; status counts read the status section; a count without a source fails the build |
status section items | a path to a b10x-status/1 file | the strip and <StatusTable data={…}/> read the same file |
related section tools[] | {id, relation, via?}, id canon, els, loom, commission, ess, a2a; relation uses, used-by, beside, specifies, specified-by | name, mark, hue, description and link from the family registry |
features items[].icon | a protocol kind | the kind's glyph heads the card |
More columns: swipe horizontally, or focus the table and use the arrow keys.
With product set, the footer gains the family strip (current product highlighted) and a "This
build" line naming the docs-system commit (from B10X_DOCS_SYSTEM_REVISION, the site's
package-lock.json pin, or this checkout), and the navbar wordmark gets a product switcher; drop the
hand-written family footer column. Documentation pages whose front matter sets status
(shipped, decided, planned), lede or source (with optional source_url and kicker) open
with a page header whose kicker is "Category · n of m" from the sidebar ("n of m" is dropped for a
one-page category; a front-matter kicker replaces the whole kicker); other pages are unchanged.
With trailingSlash: false the build writes x/index.html for every x.html: a copy of the page with
a canonical link to x that moves to x only when the path ends in /, so no server order can make
it redirect to itself (trailingSlashRedirects: false turns it off).
Every input that can be derived is generated by the owning repository's Rust generator, never typed:
| Input | Format | Derived from | Schema subpath |
|---|---|---|---|
| Terminal session | b10x-terminal/1 | running the commands | schema/terminal/v1 |
| Protocol graph | b10x-protocol-graph/1 | canon compile (canon-ir/1) | schema/protocol-graph/v1 |
| Domain graph | b10x-domain-graph/1 | ess specify compile | schema/domain-graph/v1 |
| Status | b10x-status/1 | the repository's status record | schema/status/v1 |
| Case card | b10x-case/1 | evaluator runs, for example canon evaluate | schema/case/v1 |
| Code pair | b10x-code-pair/1 | a source excerpt and the generated excerpt, quoted verbatim | schema/code-pair/v1 |
| Session charts | session-composition-instance/0-dummy | a session record | schema/session-composition/v0 |
More columns: swipe horizontally, or focus the table and use the arrow keys.
A protocol graph has nodes of kind action, evidence, claim, outcome and obligation (sorted
by kind, then name; ids <kind>:<name>) and edges produces (action → evidence), establishes
(evidence → claim, qualifier = result), supports (claim → claim), requires (claim → outcome)
and gates (claim → action, a precondition, traced on hover only). Predicates keep the canon-ir/1
shape (all, any, not, claim, evidence, input, first, before, after, since).
The order forms come from Canon 0.3.0's canon-ir/1, for protocols whose obligations depend on the
order of evidence. {"first": {"kind": "test_result", "result": "fail"}} holds when the first
evidence of that kind has that result, and reads as first evidence = fail: test_result.
{"before": {"first": "test_result", "then": "implementation_change"}} holds when evidence of the
first kind precedes evidence of the second, and reads as evidence test_result before implementation_change. Either may name an artifact in subject (a non-empty string, written only
when present), which the line ends with as about <subject>. kind and result, and first and
then, are required non-empty strings; no other field is accepted. parseProtocolGraph refuses a
before whose first and then name the same kind, naming the path; the schema cannot compare the
two and accepts it. Both forms go wherever a predicate does, including an obligation's
applicable_when and inside all, any and not.
Canon 0.7.0 adds a third order form, for a rule that reads a later record.
{"after": {"first": "tests_change", "then": {"kind": "test_result", "result": "fail"}}} holds when
evidence of the later kind with that result was observed after the first evidence of the first
kind, and reads as evidence = fail: test_result after tests_change. first, then.kind and
then.result are required non-empty strings, subject is optional as above, and no other field is
accepted at either level. The two kinds may be the same. It goes wherever a predicate does.
Canon 0.8.0 adds a fourth, for a rule that reads a record since the latest one of a kind.
{"since": {"last": "tests_change", "then": {"kind": "test_result", "result": "fail"}}} holds when
evidence of the later kind with that result, at the same subject revision as the latest evidence of
the last kind, was observed after that record, and reads as evidence = fail: test_result since tests_change. last, then.kind and then.result are required non-empty strings, subject is
optional as above, and no other field is accepted at either level. The two kinds may be the same.
It goes wherever a predicate does.
Case inputs follow canon-ir/1 too, so one protocol can describe several kinds of case. An optional
top-level case declares them: {"inputs": {"change_kind": {"description": "…" or null, "values": ["bugfix", "feature"]}}}. An input predicate leaf tests one, {"input": {"id": "change_kind", "in": ["bugfix"]}}, and reads as input change_kind in bugfix; it may name only a declared input and
its declared values. A node of kind obligation may carry applicable_when, a predicate saying when
the obligation applies; any other kind carrying it is refused. The figure lists the case inputs and
their values under the caption, and shows an obligation's "applicable when" lines before the
predicate that discharges it, in the tooltip and in the text listing. A graph without these fields
validates and renders as before.
The graphs use the layered layout in graph-layout: the caller fixes the columns, the layout inserts
dummy nodes for long edges, reduces crossings by barycentric sweeps and transposition, and places
nodes by exact isotonic regression. It is synchronous and pure, so the static HTML already contains
the finished SVG. ELK.js was not used because it lays out only asynchronously (no prerender) and adds
about 1.4 MB; dagre cannot pin columns by kind without phantom nodes.
Node-kind colours follow the dataviz reference palette, validated per mode: adjacent CVD ΔE ≥ 9.1 light and ≥ 8.4 dark. The session charts colour protocols blue, amber, rose and green, validated all-pairs because any two protocols can sit side by side in a timeline: normal-vision ΔE ≥ 19.6 light and ≥ 19.3 dark; the dark amber/green pair sits in the CVD 6–8 band (ΔE 6.9), so bars keep a surface gap and every chart also carries a text or table view.
examples/product-site is the reference site: every component with real inputs (Canon protocols,
the Commission ESS model, a recorded canon session, recorded canon evaluate and ess generate
excerpts, a role-named session record), served below /product-site/ with trailingSlash: false.
Build it from a Docusaurus 3.10 installation with docusaurus build --out-dir <dir> in that
directory; with this checkout as the installation, link it first
(ln -s ../.. node_modules/@beyond10x/docs-system).
ESS contract viewer
EssContractViewer renders ESS's existing ess-docs/1 documentation projection. It provides page
navigation, searchable declarations, typed cross references, lifecycle and relationship diagrams,
shareable section links, and source provenance. It is passive: it executes no command, accepts no
credential, and makes no claim that a declared behavior is implemented.
Generate the input with ESS, using the shared output root:
ess generate --path systems/example --kind docs-ir --out generated
In a Docusaurus host using the shared tokens and Mermaid theme:
import {EssContractViewer} from '@beyond10x/docs-system/ess-contract-viewer';
import contract from './generated/docs-ir/document.json';
export function ContractReference() {
return <EssContractViewer document={contract} id="example" sourceUrl="/data/example/document.json" />;
}
Keep id stable and unique when embedding several viewers. title, sourceRepository, and
initialPage are optional. Use Mermaid's securityLevel: 'strict' and flowchart.htmlLabels: false;
active directives and external diagram resources are refused by the document reader. Unknown link
destinations remain visibly unresolved. Unsupported document formats and malformed structures render
an error instead of a partial contract. Provenance is displayed as supplied by ESS; the viewer does
not recompute model digests.
Node preparation code imports parseEssDocument, essText, and resolveEssTarget from the
Node-safe @beyond10x/docs-system/ess-document subpath. Declare the generated JSON in source.data.include. A v4 manifest can also name
EssContractViewer in source.components; the immutable v3 component vocabulary stays unchanged. Website recognizes
ess-docs/1 data and supplies the shared viewer, a static contents summary, and the exact JSON
download. Other data catalogs retain their existing rendering.
The unit suite consumes complete ESS 0.77.0 projections from Mandate and Connectors. Browser checks build a standalone Docusaurus fixture in this repository and exercise both consumers:
npx playwright install chromium
npm run test:browser
Set B10X_CHROME_BIN to use an installed Chromium browser. ESS_VIEWER_PREVIEW_URL can instead
exercise a running Mandate preview with a /contracts/ route. The browser test is separate from
npm run gate because it requires a browser installation.
Apache-2.0. See LICENSE.