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 }
---
typeis 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, sotypemust be the one its.genre.yamldeclares, and thegenre-conformancehook 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.statusisdraft,stableordeprecated. Absent means stable.generated.bynames whatever did the writing:human:<name>for a person,<harness>/<model>for an agent such asopencode/claude-opus-5,process:<id>for a job. A person's identifier is their name, matching their page underpeople/, never a GitHub handle, which belongs to one account on one forge rather than to a human.generated.atis an ISO 8601 timestamp. Dates elsewhere, instale_afterandsources[].last_modified, are plainYYYY-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:
- What this genre is, in a sentence or two, matching the genre note its concepts carry.
- 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. - 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.
Links¶
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 theno-obsidian-embedshook.
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 ; 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.mdentry 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.
Backlinks¶
The following pages link to this page: