Skip to content

Editing Conventions

How content is written here. This page is the authority; the pre-commit hooks and CI enforce what can be checked mechanically, and the rest is convention.

Most of it follows from one fact: docs/ is an Open Knowledge Format bundle, so every Markdown file in it is a concept and carries frontmatter accordingly.

Where a file goes

Directory Holds Published
docs/ The bundle. Concepts, one per idea Yes
about/ Pages describing the site rather than carrying knowledge, like this one Yes
blog/ Blog entries about the site, rather than carrying knowledge Yes

The bundle root holds concepts and nothing else. A page that describes the site cannot live there, because the format admits no exceptions: every non-reserved Markdown file under docs/ is a concept. That is why this page sits in about/.

Concepts

Every concept requires the fields listed in docs/fkb.yaml:

---
type: Principle
title: A short, declarative title
description: A single sentence summarising the concept.
tags: [example]
status: stable
generated: { by: human:felix_schindler, at: 2026-09-07T10:00:00Z }
---
  • type is free text describing what kind of thing this is: Principle, Person, Decision, Investigation. It is not free per page, though: one directory holds one genre, so type must be the one its .genre.yaml declares, and the genre-conformance hook says so. Every directory names its own genre, so the type and the directory say the same thing twice on purpose, and a generic type is a sign the directory has not decided what it holds.
  • status is draft, stable or deprecated. Absent means stable.
  • generated.by names whatever did the writing: human:<name> for a person, <harness>/<model> for an agent such as opencode/claude-opus-5, process:<id> for a job. A person's identifier is their name, matching their page under people/, never a GitHub handle, which belongs to one account on one forge rather than to a human.
  • generated.at is an ISO 8601 timestamp. Dates elsewhere, in stale_after and sources[].last_modified, are plain YYYY-MM-DD.

Never write verified:. Its absence is how the format records that nobody has confirmed the content. It is added by whoever checks a concept against its sources, never by the author about their own work.

What the reader sees of it

Every field above reaches the page, most of them through the metadata card at the foot of the table-of-contents column, which the tech stack describes:

Field Rendered as
type the genre note at the top of the body, and the first line of the card
title the page heading
description the page's <meta> description, for search engines and link previews
tags the tag pills above the heading, and the tag listing
status "Draft" or "Deprecated" in the card, and in the genre note at the top of the body, which changes colour with it. stable renders nothing, being the default
generated "Generated by <harness>/<model>", or "Written by <person>" linking their page
verified "Verified by <person>", and a muted "Not verified" when it is absent
stale_after "Revisit after <date>"
sources footnotes, at the bottom of the page

Two of those are worth stating plainly. An absent verified renders, quietly, because an absence nothing renders is an absence no reader can read, and "nobody has confirmed this" is the most useful thing this bundle can tell a stranger. And sources[] renders only where the prose cites it, as a footnote: write [^uv-docs] at the claim the source supports, and hooks/concept_sources.py writes the definition from the frontmatter. A declared source nothing cites, and a citation with no declaration, both fail the source-citations hook.

Reserved pages

docs/index.md and docs/log.md are reserved by the format. Neither is a concept and neither takes frontmatter, except that index.md carries okf_version because it sits at the bundle root.

Every directory carries its own index.md, and the root one does not list concepts. A directory index is where a genre is defined: what kind of page belongs here, the conventions that apply to it, and the listing of its pages, ordered however suits that genre. The root index says what the bundle is, how to read it, and links to each section in the order the ideas build. See Directory indexes below.

Write a listing entry in index voice: shorter than the concept's own description, tuned to being scanned in a list rather than read alone. Copying the description across would store the same sentence twice, and reads worse in both places.

log.md records changes newest first, under a ## YYYY-MM-DD heading per day. When adding to the log, find today's heading or create one at the top; do not append at the bottom.

A log entry records what changed, never why. Added, moved, renamed, dropped, and the page it happened to. The reasoning belongs to the concept the change produced, and never in the log. Do not look at other log entries, they might not comply.

Log entries name a concept in plain text and never link to one. The log is append-only: an entry stays true after its subject is renamed, moved or deleted, whilst a link does not, and there is no good way to react to that. This is the one place in the bundle where a link is wrong; index.md on the other hand, which describes the present rather than the past, must link and must resolve.

A concept never links into about/ or blog/. The bundle has to make sense on its own, so it may not depend on the pages that describe the site around it. Links run the other way.

Directory indexes

One directory, one genre, one index. The directory is the unit because the folder axis carries the nature of a page (see Split orthogonal classification axes across folders and tags), so a directory and a genre are the same thing seen twice, and the index is where that thing gets described.

A directory index holds three things, in this order:

  1. What this genre is, in a sentence or two, matching the genre note its concepts carry.
  2. What we do here: the conventions local to this genre. Findings lead their filename with a date and carry stale_after; decisions are written wish-first. Where a convention rests on a reusable argument, link the concept that makes it rather than restating it.
  3. The listing, ordered however this genre reads best. Derivation order for decisions, newest first for findings, and so on. Say which, if it is not obvious.

The split between (1)-(2) and a concept is worth holding onto: the index says what we do here, a concept says why anyone would. A directory index is a reserved file, so it carries no frontmatter and cannot be typed, tagged, cited in sources[] or verified. Anything in it that would survive being read by a stranger with a different knowledge base is a concept in the wrong place.

The root index.md therefore lists sections rather than pages, which keeps it a page a reader can hold in their head as the bundle grows. It also carries the one thing no section can: what the bundle is for, and the order the sections build in.

Give each directory a .pages file whose title matches its index heading, so the nav and the page agree, and list the directories in the root .pages in the same order as the root index. Give it a .genre.yaml too, declaring the genre its pages carry; see the genre note below.

Page structure

Start the frontmatter on the first line, and start the body at ##.

Do not repeat the title as a heading. MkDocs renders the frontmatter title as the page heading, so a body # Title produces a second one and stores the same string twice, where the two can drift.

Every concept opens with its genre, and nobody writes it

The first thing in the body of a rendered concept (not in the sources) is a note naming what kind of page this is and linking to the index that defines the kind:

!!! note "This is an [exploration](index.md)"
    Something I committed to, built on, and withdrew from. It is a record of what the
    work taught, not a description of how anything is done now.

Do not write it into the page. It is declared once per directory, in that directory's .genre.yaml, and hooks/concept_genre.py renders it onto every concept in the directory:

type: Exploration
article: an
word: exploration
note: |
  Something I committed to, built on, and withdrew from. It is a record of what the
  work taught, not a description of how anything is done now.

One directory holds one genre. The declaration also fixes the type every concept in the directory must carry, which is all enforced by the genre-conformance hook.

Write the declaration in the voice of the directory's index, whose opening it should match.

Two constructs are forbidden outright, and a hook rejects them. Thematic breaks, because headings already separate sections and a rule line renders as a second, redundant divider; all three spellings count (---, ***, ___, and their spaced forms), since they render to the same <hr>. Frontmatter delimiters and table rows are of course exempt. And the em dash (U+2014), because - is typeable on any keyboard and greps the same way everywhere. Both are ignored inside fenced code blocks, where a snippet quotes something else's syntax.

Voice

British English throughout: "ise" endings, "our" endings, "whilst" rather than "while", no Oxford comma. Write for a technical reader.

The register is the one Write in a calm, quantified, settled-fact voice - not a promotional one describes: a maintainer standing next to the reader, narrating what happened as settled fact, volunteering the real costs, naming what a thing actually does rather than what it is for.

That page is one of three, and they guard different things. Prose that satisfies the first two can still be hard work, so write against all three:

Page Guards against Question it asks
Write in a calm, quantified, settled-fact voice dishonest language Is this overselling?
Name the concrete behaviour, not its abstract label jargon-dense honest language Is this the right word?
Hand the reader one idea at a time correct language delivered too fast How much arrives at once?

The third is the one this page kept losing, because a paragraph can be honest, concrete and still deliver three ideas in one sentence to a reader holding one. It applies to reference prose as much as to a guide: a tool page is read by someone meeting the tool for the first time.

It is not "warm", and aiming at warmth produces the opposite. Warmth is the cheapest register to imitate, so anything asked to be warm reaches for enthusiasm, second-person chumminess and exclamation, which is precisely the prose this bundle is trying not to contain. What makes these pages read as written by a person is specificity and restraint: a page that names what it gave up, quantifies where it can, and declines to hedge. Concreteness is expensive to fake; warmth is free.

Who is speaking

A concept is narrated in the third person, about named subjects. Who wrote it is recorded in generated, and rendered on the page; a pronoun is not attribution, and does not need to carry any. Write "the local copy defaults to lite", or "Felix's AGENTS.md", rather than a first person that leaves the reader to work out whose it is.

This bundle is public and published, so its concepts are quoted into other bundles and landed on from search, and a sentence whose subject is a pronoun with no antecedent degrades the moment it travels. A name survives the trip, and links to the person it refers to.

Two things keep the third person from reading like a profile of a stranger:

  • Name a person only where a person acted. An agent that reaches for a name to fill an empty subject slot writes plausible attributions nobody can check. Where no person acted, the artefact is the subject, and the sentence is usually shorter for it.
  • Name once, then let the artefact take over. The possessive carries most of it, and a page that repeats a name nine times reads worse than one that never used it.

Avoid the passive as the way out: it hides the actor without replacing them.

When the first person is licensed

The first person is available to Felix, on the genres that are about his own experience: wishes, decisions, explorations and his own person page. Those read badly in any other voice, because their subject is the person. A finding or a tool page does not, because what happened does not depend on who hit it.

Both conditions hold together: an agent never writes "I", whatever the genre, and no page outside those genres uses it, whoever wrote it. generated.by is what a reader checks the pronoun against, so the two must agree.

An agent editing a page it did not write may extend it, but never in the first person, and never by putting words in anyone's mouth. Artefact-centred sentences sit perfectly well beside "I wanted", and the page keeps one voice per author rather than acquiring a second silently. If the rewriting goes far enough that the page is no longer substantially its author's, generated changes, which is a visible act rather than a drift.

Read before you write

Before writing a new concept, read two existing pages from the directory you are writing into. The directory's index.md lists them.

This is not a courtesy step, it is the actual style control. A rule describes a voice; the existing pages are one, and prose matches nearby prose far more reliably than it satisfies an adjective. Every convention on this page put together does less to keep the bundle sounding like one author than two pages of the real thing in front of you.

What the neighbours settle is register and structure, never length. Two pages picked from a directory are whatever happened to be written there, and the thorough ones are the most inviting to imitate, so a page with nothing much to report acquires the sections it saw rather than the ones it needs. A concept is as long as its content, and a short one next to a long neighbour is the bundle working: the genre is a shape, not a quota. A heading with nothing underneath it that had to be found is the sign this went wrong.

When writing several pages in one session, re-read from the bundle rather than from what you just wrote. Otherwise the reference drifts to your own last page, and a long session ends somewhere the rest of the bundle is not.

File naming

Filenames are lowercase, with underscores between words and no whitespace:

autofix_in_the_hook.md      ✓
Autofix In The Hook.md      ✗
autofix-in-the-hook.md      ✗

Underscores rather than hyphens because a double-click selects the whole name in an editor or a terminal, where a hyphen breaks the selection. The often-repeated preference for hyphens in URLs is search-engine folklore from a decade ago; Wikipedia has served underscored addresses throughout.

Use relative Markdown links, and make sure they resolve:

[link text](../topic/some-concept.md)
[section link](some-concept.md#a-heading)

Relative paths are the only form that works everywhere at once: in an editor, on the GitHub web interface, and on the rendered site. They also survive a page moving, because MkDocs rewrites them.

What to avoid:

  • Absolute paths such as /topic/some-concept.md. The format permits them and even prefers them, but MkDocs leaves them untouched, so a moved target breaks in silence.
  • Wiki-links, [[page]]. Not standard Markdown. The shipped Obsidian settings configure link autocomplete to produce Markdown instead.
  • Note embeds, an exclamation mark followed by [[file]]. No standard Markdown equivalent, and rejected by the no-obsidian-embeds hook.

Linking to something not written yet

Do not link at a file that does not exist. The site build fails on it, so a dangling link breaks the deploy rather than leaving a helpful gap.

Write the stub instead, with status: draft and a description saying what it will contain. The link then resolves, the gap shows up in the index and in search, and rg 'status: draft' lists everything outstanding. Fill it in later and change status to stable.

Assets

Images and diagrams live beside the concept that references them, named after it:

some-concept.md
some-concept-diagram.png

Group several in a subdirectory named after the page:

some-concept.md
some-concept/
    diagram-1.excalidraw
    diagram-2.excalidraw

A concept and its pictures move together and read together. The cost is that renaming a concept means renaming its assets too.

Excalidraw diagrams

Diagrams are stored as plain .excalidraw JSON, not the Obsidian-specific .excalidraw.md wrapper. The check-excalidraw-settings hook and the shipped Obsidian plugin settings keep it that way.

Create them in Obsidian with the Excalidraw plugin, or on excalidraw.com and save the file into the repository. Embed one with ![](diagram.excalidraw); the mkdocs-excalidraw plugin renders it client-side, in light or dark mode to match the reader.

Do not commit SVG or PNG exports of a diagram. Rendering happens from the source file.

What enforces this

Rule Enforced by
The frontmatter fields above okf-concepts hook, reading docs/fkb.yaml
type matches the directory's genre, and no note is written out genre-conformance
Sources are cited, and citations are declared source-citations, and the site build
Every concept reachable from an index okf-bundle, on pull requests
Links resolve mkdocs build --strict, and linkspector
Filenames, embeds, diagram format The hooks named above
The greppable prose tells of LLM register prose-tells
One top-level heading per page markdownlint-cli2

A rule no hook checks is still a rule. prose-tells covers only what a regex can decide: the hype words, the throat-clearing openers, the future-promise framing and the corpus tells. Whether a paragraph carries one idea or three is a judgement, and stays a review pass. See the development environment for running these locally.

Committing

Commit after every logical change, and do not leave work sitting uncommitted. This repository has a remote, so a commit is not the only thing standing between a change and losing it, but it is what makes a bad edit cheap to undo, and it is the unit a reviewer reads before anything is published. A branch carrying a day of mixed work is reviewed as a wall; the same work in six commits is reviewed as six decisions.

A logical change is a concept and everything that moves with it: the page itself, its assets, the entry in the nearest index.md, and the log.md line. That last one is the marker in practice, since roughly every log entry corresponds to one commit. Several write calls whilst drafting a single page are one change, not three.

Two consequences worth stating, because both have gone wrong here:

  • A rename is one commit, not two. The title, the filename, every inbound link's target and its link text move together. Split across commits, the intermediate state is a bundle that renders a page under one name and refers to it by another, and no hook sees it because every link still resolves.
  • A commit that deletes a concept also removes its index entry. Leaving that for later breaks the build from a line nobody is editing. The log.md entry that added it stays, degraded to plain text; see the log format above.

When a hook rewrites a file during the commit, stage what it changed and amend rather than adding a follow-up commit, so the fix lands in the commit that needed it. When a hook fails outright, fix the cause and amend the same way; never pass --no-verify.

The following pages link to this page: