Peios Learn
Products
PePeios pkpekit PvProvium UDUniversal Directory TrTrail Ckclackatoo PrProject WiWispist
Using Peios Security Basics Technical Documentation Source
Using Peios Security Basics Technical Documentation Source
Trail

Writing

Single-page view · as markdown

Articles and frontmatter

Trail / Writing

An article is a single .md file named <order>--<slug>.md: a YAML frontmatter block, then markdown.

---
title: Quick start
type: how-to
description: Build and package a tiny "hello" program with pekit, from an empty directory.
related:
  - pekit/getting-started/what-is-pekit
  - pekit/recipes/anatomy
---

This page takes you from an empty directory to a working `.peipkg`…

The frontmatter is required and must be the first thing in the file: a line of exactly ---, the YAML, and a closing ---. A file that does not start with ---, or whose block is never closed, is a build error. So is any key not listed below — a typo'd descripton: fails the build rather than silently doing nothing.

The keys #

KeyRequired
titleyesThe page's name: its h1, its breadcrumb, its sidebar entry, its search-result heading, its og:title.
typein topicsThe learn taxonomy — concept, how-to, reference on this site. An error inside a book.
descriptionnoOne sentence. Becomes the page's meta description and social-preview text.
relatednoPage references shown as a "Related Content" list at the foot of the page.
inline_refnoPhrases that auto-link to this page wherever prose states them.
updatednoAn ISO YYYY-MM-DD date.
reading_minutesnoOverrides the estimated reading time.

title #

The only key every article must have, in topics and books alike. It is not derived from the slug — unlike a topic or chapter title, an article states its own.

type #

Required on every article in a topic, and rejected on every article in a book.

Trail does not constrain the value or display it on the page; it is carried through to site.json for tooling that wants to filter by it. What it is really for is the author: deciding whether a page is a concept (explaining how something works), a how-to (walking through a task) or a reference (exhaustive, looked-up rather than read) is a genuinely useful discipline, and pages that cannot answer the question are usually two pages.

Inside a book the question is already answered — a specification is read in order — so trail rejects the key rather than let it sit there meaning nothing.

description #

Optional, but write one. It becomes:

  • the page's <meta name="description"> and og:description, which is what search engines and chat apps show;
  • the standfirst line in the page's markdown mirror;
  • the description field in site.json.

It is not displayed on the page itself, and it is not the in-site search snippet — Pagefind builds those from the body text around the match.

Build with --strict to make a missing description a build failure. Alias pages are exempt, since the original carries it.

related #

A list of page references, in the same grammar body links use but without the leading ~:

related:
  - pekit/running/invocation
  - peios/package-management/overview

They render as a "Related Content" list under the article, and they appear in the page's markdown mirror and in site.json. Resolution is as strict as a body link: an ambiguous reference always fails the build, and a missing one fails unless --allow-dangling-links is set, in which case it warns and drops from the list.

inline_ref #

Phrases that should become links to this article wherever they appear in the site's prose:

inline_ref:
  - PCDS GUID
  - PCDS GUIDs

Every occurrence in ordinary prose, anywhere on the site, links here — except on this page itself. Phrases are claimed site-wide and exclusively; two pages claiming the same phrase is a build error. See Inline references.

updated #

An ISO date, and validated as one — 2026-05-01, not May 2026 and not 01/05/2026.

updated: 2026-05-01

It shows in the page's meta line, and it becomes the page's <lastmod> in sitemap.xml. It is deliberately manual: file modification times are noise (a whitespace fix is not an update, and a checkout rewrites them all), and trail does not read git history. An article with no updated: simply does not show one.

reading_minutes #

Trail estimates reading time from the body's word count at 200 words a minute, rounded up, with a floor of one minute. Code blocks count — they are read, just differently.

Set reading_minutes: when the estimate is wrong: a dense reference table reads far slower than its word count suggests, and a page that is mostly a long code listing far faster.

reading_minutes: 30

Roll-ups #

Reading times and dates add up the tree. A subfolder totals its articles; a topic totals its articles and subfolders; a book totals its chapters; an anthology totals its topics and books; a product totals everything in it.

  • Reading time is the sum, so a book's cover can say how long the whole book takes and a topic card can say how long the topic is.
  • updated is the most recent date anywhere beneath, so a container reflects its freshest page.

Roll-ups are computed after .link aliases are resolved, so an aliased article counts in the totals of the place it was linked into.

The body #

Everything after the closing --- is CommonMark with GitHub extensions: tables, strikethrough, footnotes, and > [!NOTE]-style admonitions. On top of that, trail adds:

  • ~page references — link by page identity instead of URL.
  • Co-located images — ![alt](diagram.svg) beside the article, with dimensions filled in and captions turned into figures.
  • Syntax-highlighted code blocks with copy buttons.
  • Tab groups and mermaid diagrams.
  • Inline references, § section citations and RFC-2119 keyword highlighting.

A few things happen to headings automatically: h2 and h3 become the "On this page" list, every heading gets a stable id and a permalink anchor, and inside a book they carry their section numbers as real text. Ids come from the heading text, with a numeric suffix if two headings on a page would otherwise collide.

Tables are wrapped in their own horizontally-scrolling container, so a wide table never makes the whole page scroll sideways.

Naming and ordering #

The filename is not decoration:

200--quick-start.md
 │        └── the URL segment, and what you write in a ~link
 └── the sort key among siblings — never shown, never in a URL

Two files sharing an order in the same directory is an error. Numbering in hundreds leaves room to insert later; inside a book the prefix is the published section number, so number from 1 there instead.

print is a reserved slug — every container publishes a /print page — so 100--print.md is rejected.

Links and references

Trail / Writing

Trail links pages by identity, not by URL. Instead of writing a path that breaks the moment a page moves, you write a ~ reference naming the product and enough of the page's slugs to be unambiguous:

See [Commands and targets](~pekit/running/commands-and-targets) first.

Reorganise the tree — move that topic into an anthology, wrap it in a shelf, renumber it — and the link still resolves. Delete the target, and the build fails and tells you every page that pointed at it.

The grammar #

~<product-slug>[/<segment>]…[#<fragment>]
  • The first segment is always a product slug. There is no relative form and no site-wide search; every reference says which product it means.
  • The remaining segments match the end of a page's path. Trail compares them against each page's slugs — the anthology slugs, the topic slug, any subfolder or chapter slugs, and the page's own — and matches if your segments are the tail of that list.
  • Exactly one page must match. Zero or several is an error.
  • A fragment may follow, and is appended to the resolved URL: ~pekit/reference/cli#global-flags. It is checked too: the target article must define that anchor — a heading id, a [*name] citation anchor, or an id written in raw HTML — or the link is broken. Fragments on product and anthology pages, which have no body, are not checked.

The rule of thumb is: write the shortest thing that is unambiguous, and add segments from the left only when you have to.

A worked example #

Given this page:

/peios/security-fundamentals/identity/tokens/issuing
        └ anthology ───────┘ └ topic ┘ └folder┘ └page┘

all of these resolve to it:

~peios/issuing
~peios/tokens/issuing
~peios/identity/tokens/issuing
~peios/security-fundamentals/identity/tokens/issuing

~peios/issuing is the one to write — until a second page in peios is also called issuing, at which point that reference becomes ambiguous, the build fails, and you add a segment.

Note that the deeper segments are for disambiguation only. A chapter or subfolder slug can appear in a reference even though it is not a page you could link to on its own.

What can be a target #

TargetableReference
A productyes~pekit
An anthologyyes~peios/security-fundamentals
A book coveryes~peios/pgss
An articleyes~pekit/reference/cli
A topicnolink to an article in it
A topic subfolderno(except as a .link target)
A book chapternolink to an article in it
A shelfnoit has no page at all

Topics, subfolders and chapters have no page of their own, so there is nothing for a reference to land on. Link to the article you actually mean.

Ordinary links still work #

A ~ at the start of a destination is what makes it a reference. Everything else is left alone:

[the CommonMark spec](https://spec.commonmark.org)     external, untouched
[the sitemap](/sitemap.xml)                            root-relative, untouched
[this section](#the-grammar)                           same-page fragment

Root-relative links are the escape hatch for the handful of URLs that are not pages — /print views, /llms.txt, /site.json. They are not checked, so they are also the way to ship a broken link. Prefer ~ wherever the target is a page.

A same-page fragment is checked, against the page's own heading ids, citation anchors and raw-HTML ids. Heading ids are derived from the heading text — lowercased, every run of non-alphanumerics collapsed to one hyphen, so ## vm:run(cmd, opts) is #vm-run-cmd-opts — and a repeated heading gets -2, -3, … appended. Inside a /print bundle the link is namespaced along with the ids it points at, so it keeps working there.

One form is always an error: a relative path, such as ../410--network-policy.md. Pages are addressed by their slugs, not their source files, so no relative path can ever reach one; the build rejects it and tells you to write a ~ reference instead.

~ is only meaningful in a link destination. In an image destination it is an error, with a message saying so: images are files, addressed relative to the article.

Where else references are used #

The same grammar appears in three other places:

# an article's frontmatter — no leading ~
related:
  - pekit/running/invocation
# a .link alias file — with the ~
target = "~pekit/running/signing-and-provenance"

# the root trail.toml's nav — with the ~, or an external URL
[[nav]]
label = "Specs"
url = "~peios/pgss"

The related: list is the odd one out in dropping the ~; everything else keeps it.

When a link breaks #

Every link in the site is checked on every build, and all the failures are reported together — one build tells you the whole story rather than stopping at the first bad reference.

Error: 3 broken links:
in article '/pekit/recipes/anatomy': link '~pekit/running/invokation' matches no page in product 'pekit'
in article '/peios/identity/sids': link '~pekti/overview' names unknown product 'pekti'
in article '/pekit/reference/cli': link '~pekit/targets' is ambiguous; candidates:
  ~pekit/running/commands-and-targets/targets
  ~pekit/reference/recipe-format/targets

There are five failure kinds, and they are not treated alike:

MeansWith --allow-dangling-links
Unknown productthe first segment is not a product slugwarning
No matchnothing in that product ends with those segmentswarning
Missing anchorthe page exists but defines no such #fragment — on a ~ reference or a same-page linkwarning
Relative paththe destination is a file path, which no page can servewarning
Ambiguoustwo or more pages matchstill fatal

Ambiguity is never downgradable. A missing target is a link that does nothing; an ambiguous one is a link that goes somewhere specific and possibly wrong, and only the author knows which page was meant.

--allow-dangling-links is for the middle of a refactor, when you have moved a hundred pages and want to see the site before fixing every reference. It is not a setting to leave on.

References in print views and mirrors #

The single-page and markdown views rewrite references so they still work away from the site:

  • In a /print bundle, a reference to a page inside the same bundle becomes an in-document anchor, so the whole document navigates itself. A reference to a page outside it becomes an absolute URL when the site config has a url, and stays root-relative otherwise.
  • In a .md mirror, a reference becomes the target's own .md URL, so a reader following links through the markdown surface stays in it.

Images

Trail / Writing

Images live beside the articles that use them. There is no assets directory to maintain and no separate URL scheme to remember: drop the file in the same folder as the .md, and reference it by name.

2--logon.topic/
├── 100--overview.md
└── logon-flow.svg
![The logon sequence](logon-flow.svg)

The image publishes at the URL of the directory it was found in, keeping its own file name — here /peios/logon/logon-flow.svg. Move the folder and the image moves with it.

Referencing #

Destinations resolve relative to the article's own .md file, exactly as an editor's markdown preview resolves them. Relative paths may climb out of a subfolder with .., so a diagram shared by a folder's worth of articles can sit one level up:

2--concepts.topic/
├── shared-diagram.svg
└── 300--replication/
    ├── 100--topology.md          ![](../shared-diagram.svg)
    └── 200--conflicts.md         ![](../shared-diagram.svg)

Three kinds of destination are left exactly as written and never checked: absolute URLs (https://…), root-relative paths (/…) and data: URIs.

A ~ reference in an image destination is a build error. ~ names pages, not files.

Where images may sit #

Anywhere articles may sit: a topic, a topic subfolder, a book, or a book chapter. Not in a product, anthology or shelf directory — those hold only trail.toml and subdirectories.

The file name must be a valid slug plus a lowercase extension: logon-flow.svg, not Logon Flow.SVG. Recognised extensions are:

png   jpg   jpeg   gif   svg   webp   avif

Anything else in an article directory is an "unexpected file" error — which is the point. A .pdf or a .xlsx sitting in a content folder is almost always a mistake, and trail would rather say so than quietly ignore it.

Dimensions #

Trail reads each raster image's header at build time and emits real width and height attributes:

<img src="/peios/logon/screenshot.png" alt="The logon prompt" width="1280" height="720">

The browser can then reserve the right space before the file arrives, so text does not jump around as images load. SVGs get no dimensions — they scale — and neither does any file whose header cannot be read.

Captions and figures #

An image with a title — the quoted string after the destination — that sits alone in its own paragraph becomes a <figure> with a <figcaption>:

![Three services exchanging tokens](logon-flow.svg "How a logon token reaches the session service")
<figure>
  <img src="/peios/logon/logon-flow.svg" alt="Three services exchanging tokens">
  <figcaption>How a logon token reaches the session service</figcaption>
</figure>

The two strings do different jobs and should say different things. The alt text describes the image for someone who cannot see it. The caption is prose everyone reads, and can say what the diagram is for.

An image without a title, or one sharing its paragraph with other text, renders as an ordinary inline <img>.

Unreferenced images #

Only images some article actually references are copied into the output. One that nothing references prints a warning and is left out:

warning: image '/…/2--logon.topic/old-diagram.png' is not referenced by any article and was not published

A warning rather than an error, deliberately: adding the file before the article that will use it is a normal way to work. But a warning that stays warning is a file to delete.

A referenced image that does not exist is the other way round — a broken link, fatal unless --allow-dangling-links downgrades it:

in article '/peios/logon/overview': image 'logon-flow.svg' not found (resolved relative to the article's folder)

In the other views #

Images work in the markdown mirrors and /print bundles too: since those serve an article's body away from its own folder, relative destinations are rewritten to the image's published URL.

Diagrams #

For flowcharts, sequence diagrams and the like, consider a mermaid code block instead of an image file. Mermaid diagrams are text — diffable, editable, and they follow the reader's light or dark theme, which a checked-in PNG cannot.

Code blocks

Trail / Writing

Fenced code blocks work as they do in any markdown, with an info string naming the language:

```rust
fn main() {
    println!("hello");
}
```

Trail highlights that at build time and wraps it with a copy button:

<div class="code-block" data-language="rust">
  <button class="copy-code" type="button" data-copy-code>…</button>
  <pre><code><span class="hl-storage hl-type hl-function">fn</span> …</code></pre>
</div>

Highlighting is classes, not colours #

Highlighting happens once, when the site is built — there is no highlighter shipped to the browser and no flash of unstyled code.

What is emitted is class names, not inline colours: hl-keyword, hl-string, hl-comment and so on, prefixed so they cannot collide with the theme's own classes. Those classes resolve through the same CSS custom properties as everything else on the page, which is why code follows the reader's light or dark choice instead of freezing one palette into the markup. Theming covers the tokens involved.

The copy button #

Every code block gets one. It sits in the top-right corner, appears on hover (and is always visible on touch devices), copies the block's text to the clipboard, and briefly says so.

Nothing is stripped from what it copies, so a $ prompt in a shell example is copied along with the command. Write shell examples without prompts if you would rather they paste cleanly.

Which languages are highlighted #

Trail bundles syntect as its highlighter, with the extended syntax collection from two-face — the same definitions bat ships, a little over two hundred of them. The info string is matched against a syntax's name or one of its file extensions, case-insensitively, so both rust and rs work.

Everything you are likely to reach for is covered:

bash / sh / fish / zsh   asm         awk          cmd / bat          c / c++ / c#
clojure    cmake         coffeescript   crystal    css / scss / sass / less / stylus
d          dart          diff         dockerfile  dotenv     elixir   elm
erlang     f#            f90          gd          gitconfig / gitignore
glsl       go / gomod    graphql      dot         groovy     haskell  hcl / terraform
html       http          idris        ini         java       js       jq
json / jsonnet           julia        kotlin      latex / bibtex      lean
lisp       llvm          lua          make        markdown   matlab   nginx
nim        nix           objective-c  ocaml       odin       orgmode  pascal
perl       php           protobuf     puppet      purescript python   qml
r          racket        rego         robot       ruby / haml / slim   rust
scala      solidity      sql          svelte      swift      systemverilog / verilog / vhdl
tcl        toml          typescript / tsx        typst       vim      vue
wgsl       xml / svg     yaml         zig

…alongside the config and system formats that turn up in documentation: crontab, fstab, passwd, resolv, syslog, ssh_config, known_hosts, .htaccess, requirements.txt, csv / tsv, manpage, strace, email.

Still absent: powershell, and the shell-session forms (console, shell-session). A console block renders as plain text — which is usually what you want anyway, since a $ prompt is not shell syntax.

An info string the set does not recognise is not an error and does not fail the build: the block renders as plain escaped code, with the same wrapper, the same copy button, and its language still recorded on it as data-language.

The definitions' licences ship with every build at /assets/LICENSE-Syntaxes.txt.

Blocks with no language #

A fence with no info string renders as escaped plain text, with the wrapper and copy button intact. Use one for output, trees and anything that is not a language:

```
built 1227 pages (8 products) → ./dist
```

Across this site the convention is text for that. The highlighter does not recognise text as a language, so it renders identically to a bare fence while reading more explicitly in the source.

Indented code blocks (four spaces) are supported too, and are treated as having no language.

Inline code #

Inline `code` is ordinary markdown and is not highlighted. It is also excluded from inline reference matching: a claimed phrase inside backticks stays plain text, so writing `PCDS GUID` when you mean the literal string does not produce a link.

In the other views #

Code blocks survive into the markdown mirrors and /print bundles as ordinary fenced blocks with their info strings intact — the mirrors reproduce your source, not the rendered HTML.

Admonitions, tabs and diagrams

Trail / Writing

Three constructs on top of ordinary markdown handle the things prose alone does badly: setting something apart, showing the same instruction several ways, and drawing.

Admonitions #

Admonitions use GitHub's alert syntax — a blockquote whose first line is a marker:

> [!IMPORTANT]
> **Pekit supersedes the old `peipkg-build` recipe system.** A pekit recipe
> is the current and only supported way to build a package.

Five kinds exist, and no others:

MarkerRenders asFor
> [!NOTE]NoteAn aside worth knowing, not worth interrupting for.
> [!TIP]TipA shortcut or a better way.
> [!IMPORTANT]ImportantSomething a reader who skims will regret missing.
> [!WARNING]WarningSomething that will go wrong.
> [!CAUTION]CautionSomething that will go irreversibly wrong.

The body is full markdown — lists, code blocks, links, all of it.

A blockquote whose first line looks like a marker but is not one of the five is a build error, not silently-rendered text:

unknown admonition type '[!WARN]' (expected NOTE, TIP, IMPORTANT, WARNING or CAUTION)

That is deliberate. A typo'd marker renders as a quotation with [!WARN] sitting at the top of it, which nobody proofreading their own page ever notices.

An ordinary blockquote — no marker — is still an ordinary blockquote.

Tab groups #

Tab groups show the same instruction several ways, so a page does not have to repeat itself per platform, per shell, or per package manager.

:::tabs
:::tab Linux
Install with your package manager:

```sh
sudo apt install trail
```

:::tab macOS

```sh
brew install trail
```

:::

The grammar is three markers, each alone on its own line:

Marker
:::tabsOpens a group.
:::tab <label>Starts a tab. The label is everything after the marker.
:::Closes the group.

The first tab is shown; the rest are hidden until selected. Each panel's body is full markdown, so a tab can hold code blocks, lists, admonitions and images.

Rules, all of them enforced at build time:

  • A group must be closed — an unclosed :::tabs is an error.
  • A group must contain at least one :::tab, and each :::tab needs a label.
  • Nothing but blank lines may appear between :::tabs and its first :::tab.
  • Groups cannot nest.
  • :::tab outside a group, or ::: with no group open, is an error.

Markers inside fenced code blocks are left alone, which is how this page can show the syntax without triggering it.

Note

Tab panels are hidden with the hidden attribute, so a reader searching the page with their browser's find-in-page will not see text in the tabs they have not opened. The site's own search indexes all of it. When printed, every panel is expanded and the buttons are dropped, so nothing is lost on paper.

Diagrams #

A code block tagged mermaid is rendered as a diagram in the browser:

```mermaid
sequenceDiagram
    Client->>Gate: request + token
    Gate->>Registry: check claims
    Registry-->>Gate: allow
    Gate-->>Client: 200
```

Any mermaid diagram type works — flowcharts, sequence diagrams, state diagrams, ER diagrams, Gantt charts, and the rest.

The mermaid library is a large script, so it is only shipped to pages that actually contain a diagram. A site with no diagrams anywhere never emits it at all.

The diagram is drawn in the reader's current theme, light or dark, chosen when the page loads. Switching theme afterwards re-styles the rest of the page but leaves already-drawn diagrams as they were until the page is reloaded.

Diagram or image? #

Prefer mermaid when the diagram is boxes, arrows and labels. It is plain text in your source: diffable, reviewable, editable by whoever is next in the file, and it follows the reader's theme.

Reach for an image when the thing is genuinely pictorial — a screenshot, a photograph, an illustration, or a diagram whose layout matters more than mermaid will let you control.

Inline references

Trail / Writing

Some terms should always be links. If there is one page explaining what a PCDS GUID is, then every "PCDS GUID" written anywhere on the site ought to point at it — and nobody wants to write that link by hand a hundred times, or notice the ninety-nine places it was forgotten.

An inline reference is a phrase a page claims. Trail then links every occurrence of that phrase in the site's prose to the claiming page, automatically.

Declaring a phrase #

Declaration is self-service: a thing declares the phrases that mean it, in its own file. There is no central glossary to maintain.

An article declares in its frontmatter:

---
title: Globally unique identifiers
type: reference
inline_ref:
  - PCDS GUID
  - PCDS GUIDs
---

Everything else declares in its trail.toml:

# 2--pgss.book/trail.toml
title = "Peios Generic System Standards"
short = "PGSS"
description = "Cross-platform system standards from the Peios Project"
inline_ref = ["PGSS", "Peios Generic System Standards"]

Phrases are matched literally, so plurals and alternative spellings are separate claims. List them all.

Where a phrase lands #

Declared onLinks to
An articlethat article
A productthe product's landing page
An anthologythe anthology's landing page
A bookthe book's cover — and § may reach a section
A topicthe topic's first article
A topic subfolderthe folder's first article
A book chapterthe chapter's first article

The pattern is: the declarer's own page, or — for the containers that have no page — the first article inside it. A container that declares a phrase but holds no articles to land on is a build error.

A shelf may not declare a phrase at all, since it has neither a page nor articles of its own. Declare it on the shelf's items.

Where phrases are matched #

In ordinary prose, and only there. Matching skips:

  • Headings, whose ids are derived from their text;
  • Link text — a phrase inside an existing link is left alone, so a deliberate link is never rewritten;
  • Image alt text;
  • Code blocks and inline code — writing `PCDS GUID` when you mean the literal string produces no link;
  • The claiming page itself, which never links to itself.

Two more rules make matching predictable:

  • Whole words only. A phrase surrounded by letters, digits or underscores does not match. "PCDS GUIDs" does not match inside "PCDS GUIDsomething".
  • Longest wins. When two claimed phrases overlap at the same position, the longer one is used — so "PCDS GUID" beats "PCDS", and a phrase does not get chopped up by a shorter one inside it.

Phrases are exclusive #

A phrase belongs to exactly one declarer, site-wide. Two things claiming the same phrase is a build error:

inline_ref phrase 'PCDS' is claimed by both book '/peios/pcds' and article '/peios/formats/pcds-overview'

That is the whole point: if two pages both want to own a term, the ambiguity is real and a reader would have been sent to the wrong one. Decide which page is the destination.

Empty phrases, phrases with leading or trailing whitespace, and phrases containing § are also errors. Link aliases never claim phrases — the original keeps them.

Choosing phrases well #

Inline references are powerful in the way find-and-replace is powerful. A phrase that is too common turns the site into a sea of blue.

  • Claim terms of art, not ordinary words. "PCDS GUID" is a term. "identity" is a word.
  • Claim what a reader would want defined. The test is whether someone meeting the phrase mid-sentence would benefit from a link.
  • Prefer the specific form. Claiming "GUID" catches every passing mention; claiming "PCDS GUID" catches the ones that mean your page.
  • Remember plurals and expansions — they are separate claims, and a term claimed in only one of its forms links inconsistently, which reads worse than not linking at all.

Section citations with § #

A phrase claimed by a book can be extended with a section number in prose:

Every token MUST carry a lifetime, per PGSS §2.1.

"PGSS §2.1" becomes one link to that exact section. The suffix is a single space (or non-breaking space), the § sign, and a section label — digits, dots and appendix letters: §2, §2.1.3, §A, §B.2. A sentence-ending full stop is not part of the label.

Every book carries an index of its section numbers, covering:

  • articles, by their section number (§2.1 → that article's page);
  • chapters, by theirs (§2 → the chapter's first article);
  • headings inside articles — the h2 and h3 numbers trail generates (§2.1.3 → that article, at that heading).

So a citation can be as precise as a subsection of a page.

A § label that does not resolve is a build error — with the phrase still linked to the book, so the page is not broken while you fix it:

'PGSS §9.9' does not match any section of '/peios/pgss'

Like a broken ~link, it is downgraded to a warning by --allow-dangling-links.

Bare § inside a book #

Within a book's own pages, a bare § reference resolves against the surrounding book, with no phrase needed:

The requirements of §2.2.1 apply, except as noted in §A.

Unlike the phrase-qualified form, a bare § that does not resolve stays plain text and says nothing. That is not an oversight: specifications routinely cite sections of other documents, and a build that failed on every "§9.9 of RFC 3986" would be unusable. The qualified form names its book and so can insist; the bare form cannot, so it does not.

RFC-2119 keywords #

Requirement keywords are highlighted everywhere on the site, with no configuration and nothing to declare:

MUST · MUST NOT · REQUIRED · SHALL · SHALL NOT · SHOULD · SHOULD NOT · RECOMMENDED · NOT RECOMMENDED · MAY · OPTIONAL

They must be written in capitals, as RFC 2119 intends — the capitals are what distinguishes a requirement from the ordinary English word. Matching is whole-word and longest-first, so "MUST NOT" highlights as one keyword rather than as "MUST" followed by a stray "NOT", and it skips the same places phrase matching skips: headings, links, and code.

The styling is a weight and the product's accent colour, not a badge. It is meant to make a requirement scannable in a page of prose, and it works in ordinary articles as well as in books.

Named citations

Trail / Writing

A § reference reaches an article. That is usually the right size for prose — but not for a test. A test verifies one statement, and an article holds many; "this test covers §4.6" cannot tell you which of §4.6's nine statements is still unverified.

A named citation is an anchor on a single statement:

Copy-up preserves the POSIX ownership of the object as provided by
the lower stratum. [*copy-up.preserves-ownership]

Anything outside the site can now point at exactly that sentence:

// Verifies PKM *copy-up.preserves-ownership

The build collects every anchor into citations.json, so a test suite can ask which citations nothing yet verifies.

Anchors on headings #

An anchor on a heading names the whole section:

### `ro` [*strata.ro-is-independent-of-filesystem]

This is usually what you want. A well-structured document already gives a behaviour its own section, so the section is the citable unit, and an anchor on a heading rides the structure rather than one sentence of prose that may later be rewritten.

The marker renders inside the heading and contributes nothing to the heading's id or its table-of-contents entry, so it is safe to add to an existing heading — nothing that linked to that section breaks.

In citations.json a heading anchor is reported with "kind": "section" and its context is the heading text; everything else is "kind": "statement". A coverage report wants the difference: an uncovered section is a different-sized gap from an uncovered sentence.

Reach for a statement anchor when one section states several independent rules — a table of error conditions, or a numbered list — where one name for the section would be too coarse to say which rule is untested.

An anchor is a locator #

It marks a place, not a span. [*copy-up.preserves-ownership] says "the statement here is called this"; it does not say where that statement begins or ends, and Trail never tries to work that out.

That is deliberate. A name is arbitrary, so nobody argues about it and nobody revises it. An extent looks like it has a correct answer — does the following clarifying sentence belong inside? the exception clause? — so it invites disagreement and revision, and every revision is churn that helps none of the things a citation is for. Overlapping and nested statements have no clean bracketing rules either.

So an anchor is a pointer, and a reader follows it and reads.

Note

If a paragraph holds three statements you cannot tell apart, write three paragraphs. That is a prose improvement rather than a workaround, and it makes each anchor's context exact for free. The same reasoning applies one level up, where an article covering nine unrelated rules forces every § citation to be approximate.

One citation, many tests #

Coverage is "does this citation have at least one passing test". It is never a one-to-one mapping.

A statement in a specification is often close to one testable behaviour. In a reference manual it usually is not: an anchor is as much a way of dividing a large descriptive document into consumable chunks as it is a single assertion. Getting closer to one-anchor-one-behaviour is better where it happens naturally, but it is not a requirement, and a report that assumes it will mislead.

Writing anchors #

Put the anchor at the end of the statement it names. Do it as a separate pass, after the prose is written — annotating while drafting interrupts the writing, and picking out testable statements is a different kind of attention from composing them.

A name is lowercase alphanumerics in dot- or hyphen-separated segments, starting and ending alphanumeric:

copy-up.preserves-ownership✅
mount.strata.order✅
Copy-Up.Ownership❌ uppercase
copy..up❌ doubled separator
.leading❌ must start alphanumeric

The grammar is enforced at build rather than merely recommended: a name is what every citation greps for, so a corpus with three naming styles is a corpus where a search misses things.

A name must be unique within its book, and the build fails if one is declared twice. Uniqueness is per book rather than site-wide — unlike an inline_ref phrase — because names are naturally book-scoped and a citation from outside is qualified by the book's short name anyway.

Only a book may carry an anchor. A citation is addressed through its book, so an anchor in a task-doc topic could never be cited; the build rejects it rather than leaving it silently unreachable.

Retiring one #

Delete the anchor, and grep the name to find the tests and comments that pointed at it. Do not reuse a retired name for a different statement: nothing can detect that, and every existing citation silently comes to mean something else.

How it renders #

A small superscript marker, positioned out of the text's flow so line height and wrapping are exactly as they would be without it. Clicking copies the full citation string — the thing you want in a test — rather than navigating; the link still targets the anchor, so "copy link address" and middle-click behave normally, and the page works without JavaScript.

Readers who would rather not see them at all can call trailToggleCitations(), which persists per browser.

What the build produces #

citations.json, at the site root, listing every anchor in the corpus:

{
  "citations": [
    {
      "name": "copy-up.preserves-ownership",
      "citation": "PKM *copy-up.preserves-ownership",
      "book": "/peios/advanced-peios/peios-kernel",
      "article": "/peios/advanced-peios/peios-kernel/stratafs/overview",
      "url": "/peios/advanced-peios/peios-kernel/stratafs/overview#cite-copy-up.preserves-ownership",
      "section": "4.1",
      "context": "Copy-up preserves the POSIX ownership of the object as provided by the lower stratum."
    }
  ]
}

It is its own file rather than part of site.json, which is a tree of the whole site: the consumers here want a list they can read without walking one.

context is the source block the anchor sits in, not the statement it marks — an anchor delimits nothing. Two anchors in one paragraph get the same context. Treat it as a label for a coverage report, and follow url when you need the real thing.

Anchors are also left as written in the markdown mirrors, so an agent reading print.md sees them.

Anchors in code samples stay literal #

[*name] inside a fenced block or inline code is never an anchor, which is what lets this page show the syntax. Everywhere else, an anchor is rewritten before the markdown is parsed — otherwise the * could pair with an emphasis marker elsewhere in the paragraph and swallow the anchor into an <em>.

Peios Learn — documentation for the Peios project.

Built with Trail.