Item 2 of the order of work. An author writes `../day-01.en.md` — the path an editor preview resolves — and the engine emits `/posts/day-01/`. The larger effect is durability. Resolution goes through key → route, and a slug moves the route while never moving the key (ADR-0035), so a relative link survives a rename that a hand-written /posts/a-better-name/ does not. The demo proves it: `../renamed-thing.en.md` renders as href="/posts/a-better-name/" — the author wrote the filename and got the slugged address. This is the engine altering authored markup, which ADR-0045 polices, so the test that matters is what it declines to touch. Fourteen cases must survive exactly as written: an absolute URL, a scheme-relative URL, mailto:, tel:, a root-relative path, a bare fragment, a bare query, a name climbing out of content/, and every relative path whose extension is not .md. That last line is what keeps cover.jpg working — a bundle's assets already resolve because its URL mirrors its directory, so rewriting them would break what works. Nine rewrite cases sit beside them. Key derivation goes through content.KeyFromName, exported for this: the language-suffix rule is the part that would drift between two copies, so it lives in one place while the five lines of joining are duplicated in check. khosra check now reports a relative .md link resolving to no bundle, as fatal — verified by mistyping one and watching exit 1. Only the .md form: an extensionless relative path may be an asset, and a checker that calls a working link broken gets ignored wholesale. Two debts this change paid rather than deferred. render.go reached the file-length advisory, so theme parsing moved to theme.go — 414 and 105 lines, one topic each, since parsing runs per rebuild and rendering runs per request. Not a _helpers.go shard. And the demo's coverage test bound its renderer with a *copy* of the rebuilder's wiring, so it missed this feature entirely while the real binary served it correctly. Navigation had already drifted the same way. Both now call one bind(), which is exactly what ADR-0072 was written about — and the test failing is the only reason the copy was found. Extensions 7 → 8. Core 3020 → 3049 of 3400: the seam is ~20 lines, the feature is in ext where it belongs. 18 files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Harness doc map
The docs an agent reads to build the engine. Read the one you need. Do not read them all.
This directory was docs/ until ADR-0082 renamed it: docs/ is reserved for documentation written for
whoever uses khosra, which does not exist yet. Everything here is engine- and agent-facing — a
specification, a rule, or a record — and none of it is written for an author.
| Doc | Contains | Mutability |
|---|---|---|
architecture.md |
The six primitives, invariants, per-primitive STATUS | Only via an ADR |
state.md |
What exists now: inventory, earn-it counters, latent items | Every feature |
decisions.md |
ADR log — one entry per load-bearing choice | Append-only. One exception: a superseded entry's Status line is annotated in place to name what replaced it, because a reader arriving at the older entry has no other way to learn it was replaced. Never the Decision text — that stands as history |
roadmap.md |
Arcs, earn-triggers, the core freeze point | When an arc completes |
content-model.md |
On-disk layout, frontmatter, post types, permalinks — what the parser accepts today, never a plan | When the disk format changes |
conventions.md |
Go style floor, package layout, perf and test rules | Rarely; via ADR if contested |
extensions.md |
Extension/plugin contract: target shape + earn gates | When earned or frozen |
toolchain.md |
Versions and agent-tooling contracts this was built against | When you deliberately move a version |
theme-contract.md |
What the engine promises a theme; the only theme-facing obligation | When the contract is extended — additively only |
context-economy.md |
How the agent spends context: what to read, in what form, and the floor no saving may cross | When a technique or a gate for one changes |
surface.md |
Generated. Every top-level declaration with its line — read before opening source | Never by hand; make surface |
Sibling folders sit outside harness/ because they are storage, not working material:
../ideas/ (parked ideas, any topic, and exploration.md — the catalog of engine features nobody has asked
for yet) and ../reference/ (durable facts from conversation). Write there when asked to park something;
open a file only when the human names it. Nothing sweeps them.
Topic ownership — read before you assert
The inverse of the refresh triggers: those say what to update after changing code, this says what to read before writing a rule — including when you are only writing an ADR or a comment about it. Nearly always exactly one doc; if two seem to apply, the single-source rule is already broken and that is the finding to report.
| About to assert something about… | Read first |
|---|---|
| plugins, extensions, features-as-packages, stage phases | extensions.md |
| a primitive, an invariant, or what is "earned" | architecture.md + counters in state.md |
| URL paths, permalinks, frontmatter, on-disk layout, post types | content-model.md (+ ADR-0008) |
| Go style, package layout, file/function size, test shape | conventions.md + scripts/budgets.env |
| a dependency | scripts/allowed-deps.txt + ADR-0007 |
| a budget or a gate | scripts/budgets.env + scripts/verify.sh |
| the untrusted boundary, comments, webmentions, form input | ADR-0003 + extensions.md |
| language, translation, fallback | ADR-0004 + ADR-0009 |
| deploy, containers, external services | ADR-0010 |
| whether a future feature is worth building | ideas/exploration.md + roadmap.md |
| which list owns a piece of pending work | four kinds, deliberately not merged: might be built → ideas/exploration.md — the catalogue, plus parked and dropped items each with a trigger or a reason · is scheduled → roadmap.md, whose "Order of work" says which is next · is broken and known → the Latent list in state.md · waits on an observation → ideas/deferred-decisions.md "Triggers — the one list". Merging them would flatten four levels of authority and make pending work expensive to ignore, which is the property that keeps ideas/ safe to keep |
| what to build next | the harness first (CLAUDE.md §1 read order) and git log for what landed, then .scratch/continue.md for the continuation point and roadmap.md for the arc. The handoff is temporary and ungated: check anything it calls pending with git log -- <the path it names> before planning from it |
| how the harness works — a gate, counter, budget, the loop | HARNESS.md + scripts/verify.sh + CLAUDE.md |
| what one commit contains, and when one happens | conventions.md "Git" (shape) + CLAUDE.md §4 (cadence, ADR-0052) |
| how to read, search, or report without wasting context | context-economy.md (+ ADR-0053) |
| a tool version, or why agent tooling stopped working | toolchain.md |
| templates, layout, presentation, what a theme can rely on | theme-contract.md (+ ADR-0019, ADR-0023) |
Single-source rule
Each fact lives in exactly one doc. Need it elsewhere? Link. This binds ADRs too: an ADR restating a rule an owning doc already carries is a duplicate, not a decision — delete it and amend the owner.
Values that have drifted before, and their one home. Everywhere else names the concept and points here; nothing else may state the value:
| Fact | Sole home |
|---|---|
| Earn-it thresholds (transforms, routes, views, extensions) | the counters table in state.md |
LOC ceilings, size warnings, dependency cap, the CLAUDE.md ceiling |
scripts/budgets.env |
| The permalink path shape | content-model.md (decision recorded in ADR-0008) |
| Which language is at the root, and the prefix form | ADR-0009 |
| The leaf/trunk definition | architecture.md invariant 7 |
| The sovereignty test | roadmap.md Governors |
| Package layout and the forbidden package names | conventions.md |
| What exists in the code right now | state.md inventory |
| What un-defers a deferred decision | the Triggers table in ideas/deferred-decisions.md |
A value restated in two places is not redundancy for safety; it is a future contradiction waiting for whichever copy gets edited alone.
state.md is the only doc describing the present; the rest describe rules, shapes, or intentions.
When code and state.md disagree, the code wins and state.md is wrong.
Refresh triggers
Update in the Document step of the change that caused them. Not later.
| Change | Update |
|---|---|
| Any code change at all | state.md inventory — in the same commit, which is what verify.sh compares (ADR-0057) |
| New transform, route, view, extension, or dependency | the counters table in state.md |
| A choice expensive to reverse | new ADR in decisions.md |
| A primitive becomes real (earned) | STATUS line in architecture.md + counters |
| New frontmatter field, post type, or path shape | content-model.md |
| New dependency approved | scripts/allowed-deps.txt + ADR |
| Budget raised | scripts/budgets.env + ADR — once the first feature has shipped. While the harness is still being tuned, edit in place. |
| A latent item fixed | remove from the Latent list in state.md |
| A planned item completed | after the commit lands, delete it from .scratch/continue.md and note anything learned that changes a later one. Never during Document: a handoff written mid-change describes the plan, and an amend moves the commit under it — which is how one was written at 01:06 saying a trim awaited a yes that the 01:12 commit had already done |
| Arc finished | roadmap.md + a retro line in state.md |
| Catalog item accepted or rejected | verdict line in ideas/exploration.md |
CLAUDE.md, scripts/ or .claude/ changed |
HARNESS.md, same change. Enforced by verify.sh. |
| A tool version deliberately moved, or a new external contract relied on | toolchain.md |
| The theme contract extended, or embedded templates changed | theme-contract.md, same change. Enforced by verify.sh. |
| A request spans engine and theme | contract extension here + a written note of the theme's part; never the theme itself |
| A new gate, counter, or budget added | HARNESS.md "Why the pieces exist" + a row in one of these tables |
| A top-level declaration added, renamed or moved | nothing by hand — make surface, which verify.sh then compares |
| A rule, value, name or path changed | grep the repo for the old form; every doc naming the concept, same change |
| The disk contract changed | content-model.md + testdata/ fixtures + a written migration step for the site root, which the engine repo cannot touch |
| A doc or ADR mandates what a change made optional | supersede the ADR; do not reword it in place |
| A request reversed a recorded decision | new ADR, or supersede the old one — the request does not win by being newer |
| A request exceeded a convention on purpose | note the deviation where the convention lives |
| An idea floated, not built now | a file in ../ideas/ + its index line. Adopted later: Status: adopted → <where> |
| A fact worth keeping surfaced in conversation | a file in ../reference/ + its index line, stating how it was established |
Staleness
/refresh-docs reconciles docs against code and reports drift. Run it after a burst of work,
before a new arc, and any time a doc surprises you.
House style
Present tense. Terse. Written for a reader who has forgotten everything, including their own reasoning. No changelog prose, no "recently we…", no preamble. Delete a line before rewriting it. A doc needing a table of contents is too long.
Compression contract
Shortening a harness doc removes words, never instructions. Same rules, gates, thresholds,
triggers and table rows before and after; the agent must act identically. Redundancy between
CLAUDE.md and SKILL.md is deliberate — the constitution is always loaded, the skill is not —
and is not duplication to be collapsed.
Deleting a rule, adding one, or narrowing one is a rule change: it needs its own decision and its own reason, and it never rides along in a compression pass.
Verify before reporting a saving: table rows, headings, list items and checklist boxes must match
the pre-pass file (git diff --stat, or a snapshot if the change is uncommitted). Report word count,
not lines — reflowing prose shrinks lines without saving anything.