Files
khosra/docs/state.md
T
Claude Opus 5andbdeshi 67defae912 highlight code server-side, and let a block quote a file
chroma at render time, emitting CSS classes rather than inline colour, handed to
a `code` theme fragment. Highlighting works with scripting off, in a feed
reader, in a browser that never runs JavaScript. No lighter pure-Go option
exists — every "alternative to chroma" is JavaScript, which the reference theme
is gated against.

A fence's info string carries the rest: title, numbers, start, hl=3,7-9, and
file=name lines=A-B, which reads the snippet out of a file beside the bundle and
numbers it by that file's own lines. So a post quotes several parts of one
program without the copies drifting from it, and a reader can find what they are
looking at. Verified on the real binary: the same file at lines 5-10 and 12-14,
each numbered as it really is, with different lines tinted.

Not a new package: a new one could not import the key=value parser this repo
already has, because ADR-0069 forbids a feature importing its sibling, and a
second parser for the same syntax is what §6 stops.

Two costs, both stated in the ADR rather than buried. The binary goes from ~15MB
to 19MB, for a project whose story is one small binary. And the reference theme
now carries a token palette — the first thing in it that is a taste rather than
a demonstration — kept to eight classes for that reason.

The demo quotes a shell file, not a Go one: a .go file under examples/ joins the
module and has to compile, which the build gate caught before it shipped.

6 of 9 modules, ext 2188/3500.
2026-08-02 00:16:32 +06:00

20 KiB

State

Currency is compared, not declared. verify.sh warns unless this file's last commit is at least as new as the last commit touching .go, so it ships inside the change it describes — no sha to write, and no trailing commit to write one (ADR-0057). If this file disagrees with the code, the code is right and this file is a bug.

Inventory

Line counts are not here: docs/surface.md is generated from the code and carries per-file sizes plus every declaration's location (ADR-0053). This column drifted on six files before it was removed, which is what a number written in two places always does. What each file is for is the fact this table owns.

File Purpose
go.mod module khosra; goldmark, x/text, yaml.v3 direct
internal/content/doc.go package comment
internal/content/content.go bundles: os.Root open, walk, frontmatter split, key/lang derivation, NFC, tag slugs, partial files, permalink building
internal/content/clock.go the one place the engine reads the wall clock, which verify.sh enforces by filename
internal/content/extras.go a bundle's supporting files: enumeration, classification, and their URLs (ADR-0047)
internal/content/settings.go site.yaml: the site's own declarations (base, title) and absolute-URL building (ADR-0039)
internal/content/site.go the indexed site: lookup with language fallback, aliases, Query and Run, sections, Sequence, Everything, slug routes, publication visibility
internal/render/render.go goldmark with the typographer, per-kind template sets with site override, the render methods. The parsed sets plus the stylesheet are one snapshot behind an atomic.Pointer; Refresh is the only thing that replaces it, so every page serves one theme (ADR-0055, ADR-0056). Heading ids are a parser option set here, declared or derived (ADR-0058, ADR-0066), and this is the one renderer that enables raw HTML (ADR-0060). Compose is the seam a merging bundle's splice arrives through
internal/render/view.go the theme contract in Go, and now actually all of it: Page, List, Sequence, Extras, Item, Partial, Fragment (with Body, Headings and Lang — ADR-0064, ADR-0065, ADR-0067), Heading, Picture, Origin
internal/render/chrome.go the engine's own words: phrase table, month names, digits, and the t/num/day template funcs (ADR-0034), including the words a shortcode fragment supplies when the author gives none (ADR-0067)
internal/render/templates/ reference theme, complete (six icon names map to Unicode, no assets — ADR-0063): base.html (shell, navigation, language links, feed and OpenGraph), page.html (bundle, sequence, tags, extras), list.html, extras.html, shortcodes/ — seven fragment files rather than one, and a site may use either form (ADR-0071) — with the sizes its own layout implies (ADR-0068), theme.css (ADR-0026, ADR-0049)
internal/ext/shortcodes/ first feature: ::name{key=value} block parser and node renderer, rendering through a theme fragment (ADR-0036). figure, gallery, include, plus the derivative pass and remembered picture inspection, bounded and least-recently-used in memo.go (ADR-0042, ADR-0044, ADR-0073). FootnotePrefix namespaces an included file's footnote ids (ADR-0058). Directive syntax since ADR-0059, plus icons.go: :name: inline, rendered by the theme's one icon fragment (ADR-0063), containers.go: :::name{…}::: wrapping a rendered body (ADR-0064), toc.go: the document's headings for a ::toc call, to a depth the call may set (ADR-0065, ADR-0066), and code.go: chroma highlighting, a fence's options, and snippets read from a file (ADR-0075). Merge splices includes before the parse for a bundle that asks for it (ADR-0066)
internal/ext/notation/ the inline marks CommonMark lacks: ~sub~, ^sup^, ==mark==, and ~~strike~~, which it owns so a single tilde can mean subscript (ADR-0061). abbr.go adds *[TERM]: definitions and the pass that expands them (ADR-0062)
internal/ext/scaffold/ writes one draft directory bundle into a site root through os.Root: never an overwrite
internal/ext/watch/ polls content/ and templates/ on an interval it is given, ignores editor droppings, and reports a settled change (ADR-0022, ADR-0048, ADR-0056). site.yaml is deliberately not fingerprinted (ADR-0055)
internal/ext/check/ third feature: validates a site root — what the engine worked around, broken internal links, missing titles and alt text, mixed series ordering, and calls left in the retired shortcode form (ADR-0059)
cmd/khosra/wire.go the only list of enabled features (extensions.md), the theme function that builds the renderer this build ships (ADR-0072), and the Markdown dialect with it — tables, footnotes, definition lists, strikethrough (ADR-0058)
internal/web/resolve.go URL → (key, lang, page, tag, feed, extras) or a canonical redirect
internal/web/extras.go the extras route: listing, one entry selected, or ?raw bytes, all behind the bundle lookup
internal/web/asset.go files inside a bundle's own directory, looked up through the owning bundle so visibility can only ever inherit (ADR-0024)
internal/web/feed.go Atom for the site, a section or a tag, from dated bundles via one Query (ADR-0043)
internal/web/discover.go /robots.txt and /sitemap.xml, absolute and only with a declared base (ADR-0039)
internal/web/web.go handler: serve dispatches by kind, serveBundle answers the commonest one; listings, /static/, /derived/, degrade on failure
cmd/khosra/main.go flags (including -poll, zero to stop watching), wiring, startup, the derivative pass, and the atomic swaps a change goes through — theme in the watcher's callback, index in rebuilder. main dispatches subcommands, runServe assembles the server, rebuilder is used at startup and on every change alike
cmd/khosra/check.go the check subcommand: parse, print, exit code. What counts as a finding lives in the feature
cmd/khosra/new.go the new subcommand: arguments in either order, then the feature does the writing
*_test.go table-driven, one file per source file — shortcodes has one each for icons, containers and the contents list; symlink escape (content and static), canonical paths, language fallback, aliases, pagination, tags, sequences, chrome, typography, shortcode escaping, galleries, includes, partials, site settings, absolute URLs, robots, sitemap, slug routes, bundle assets, derivatives, feeds, 404, plus benchmarks for the render path and the checker, unpublished visibility, listing shapes, scaffolding, extras, change detection, what a page can reach, the root listing, and the example site end to end — that last one in cmd/khosra, beside the wiring it proves (ADR-0072)

Serves a listing of everything at / (ADR-0050), a bundle at /{section}/{slug}/ — the slug derived, or declared in frontmatter without moving the key (ADR-0035) — a paginated listing per section, tag listings global and section-narrowed, sequence navigation and a series archive on any nested bundle, static/ verbatim, a directory bundle's own files under its URL, generated derivatives under /derived/, Atom feeds per site, section and tag, a bundle's extras as a browsable tree, plus /robots.txt and /sitemap.xml. Markdown is CommonMark plus tables, footnotes, definition lists, heading ids and the inline marks (~sub~, ^sup^, ==mark==, ~~strike~~, *[TERM]:), and nothing else (ADR-0058, ADR-0061, ADR-0062); HTML an author writes renders, because the site root is trusted (ADR-0060). Chrome text, dates and digits render in English or Bengali; authored text is untouched but for typographic smoothing (ADR-0034); line breaking is left to CSS (ADR-0045). This repo holds engine source only — the site root is external and passed with khosra check validates a site root and exits non-zero on anything that makes it wrong; khosra new scaffolds a draft bundle into one. A running server notices changes under content/ and templates/ by polling — -poll, zero to stop — and swaps both the index and the parsed theme atomically, so a content or template edit appears without a restart and every page updates together (ADR-0022, ADR-0055, ADR-0056); site.yaml applies at startup only. A draft or future-dated bundle is not served at all — nor is any file inside it (ADR-0024) — until -dev on reveals it and polls four times a second (ADR-0056). -site (ADR-0011). site.yaml declares base and title; with a base, canonical, hreflang and OpenGraph URLs go absolute (ADR-0039).

Frontmatter the parser lifts today: title, date, tags, aliases, order, slug. include: merge is read from Extra by the renderer (ADR-0066). Every other key in content-model.md's table — including draft and type — lands in Extra unread, so that table is the accepted format, not a list of what runs.

examples/demo-site/ is a complete site kept in the repository to be read and served — 30 bundles across six sections, one case per feature in TestTheExampleSiteExercisesEveryFeature, and khosra check run over it by verify.sh (ADR-0051). make demo serves it. Features are spread across the bundles where a real site would put them rather than piled on one page: text marks and the margin note on writing/typography, icons and panels on pages/colophon, the include, table, glossary and contents list on writing/notes-on-water.

A Dockerfile ships the binary alone: the site root is a mounted volume, never copied in (ADR-0010, ADR-0011).

Dependencies: five direct, all allowlisted — goldmark, golang.org/x/text, golang.org/x/image, gopkg.in/yaml.v3, chroma (ADR-0075); six modules of nine with regexp2 indirect.

Counters — the earn-it authority

Never anticipate a threshold. Increment when the code lands, then check whether the extraction is due this change.

Every row states what does not count. Four of these counters had to be re-scoped on first contact — transforms, views, effects, extensions — because each measured a symptom rather than the mechanism, and the re-scoping was always a sentence about what had been wrongly included. Writing that sentence up front is the cheapest way to find out whether a counter means anything, so the column is required and verify.sh fails on a row that leaves it empty (ADR-0070).

Counter Now Extraction due at What it buys Does not count
Render transforms — page-level only 0 3 Stage pipeline (ordered func(ctx,*Page) error). This counts transforms over the assembled page, which nothing hosts yet — OpenGraph and JSON-LD (queue 15) are the first candidates Parse-phase work. goldmark's extender list is already an ordered pipeline for it, so typography, shortcodes and notation compose there (cmd/khosra/wire.go); a second pipeline beside it would be pure duplication
Routing cases 11 2 — done Resolver at internal/web/resolve.go: bundle, language prefix, pagination, tag, section-narrowed tag An exact path a crawler asks for by name. /robots.txt and /sitemap.xml are mux entries because no bundle can collide with them, so they never reach the resolver
Collection pages 4 1 — done Query primitive: content.Query{Section, Tag, Lang} + Site.Run A series archive. Membership is structural and the sort ascends, so it resolves through Site.Sequence — sharing the index but not the Query
Views — per-bundle selection only 0 2 The View layer architecture.md describes: view: in frontmatter choosing a presentation, resolved through the cascade. Nothing selects a view yet Output formats. HTML, sitemap XML and Atom are three functions with nothing to share — an interface over them would have one member and no leverage
Effects 1 2 Effect runner + trigger wiring (change / schedule / demand). The only one is the derivative pass (ADR-0042), called from cmd inside rebuilder, so it already answers both triggers it will ever need — startup and a settled change (ADR-0048) An in-memory swap. Replacing the index or the theme re-reads the site root into memory, writing no artifact and calling nothing outbound (ADR-0055)
Extensions 4 3 — passed, and the answer is still no Extension registry (extensions.md). It reached 3 once before and went back to 2 when the widows feature was deleted (ADR-0045) — a threshold reached by a feature that should not exist was never a threshold. The note below says why a registry still buys nothing An upstream extension enabled in the list. Table, Footnote and DefinitionList are goldmark's, so they are dialect rather than features of this engine (ADR-0058) — only a package under internal/ext/ counts
Interface implementations 2 The interface itself An interface this repo did not declare. Satisfying fs.FS, http.Handler or goldmark.Extender is using somebody else's abstraction, which is the opposite of inventing one
Non-stdlib dependencies 4 direct budget in scripts/budgets.env The standard library, and a dependency's own test-only modules — go list -m all shows those, and the gate counts require entries instead (scripts/budgets.env)

The Extensions counter is past its threshold, and a registry would still buy nothing. The four features attach in three unrelated ways: shortcodes and notation are goldmark extenders listed in extenders(), check and scaffold are functions cmd calls for a subcommand, and watch is a goroutine. A registry would have to abstract over "extends Markdown", "validates content", "writes a file" and "polls a directory", which share nothing but the word feature — one member and no leverage. Adding notation made this clearer rather than more urgent: two goldmark extenders compose in goldmark's own extender list, which is already the registry for that shape, and they need no order relative to each other because one is inline and the other block. Build the registry when a feature wants a route (the seam ADR-0042 named) or when two features genuinely need to agree on an order that no existing mechanism expresses.

Allowlist, all four imported: goldmark (markdown), golang.org/x/text (NFC, ADR-0015), gopkg.in/yaml.v3 (frontmatter, ADR-0020), golang.org/x/image (resampling and WebP, ADR-0040).

Latent items — known, deliberately unfixed

Do not fix these mid-feature. They become features when the human says so. An arc does not close with an untriaged item: at each arc boundary every row is fixed, scheduled into an arc, or accepted with a stated reason. A list nothing drains is a graveyard of known defects.

Item Why it waits Trigger to fix
No mechanical check that the counters are correct Accepted at the Arc 1 boundary: the coupling gate makes forgetting them impossible, which is the real failure mode, and checking the values needs code to count The first page-level transform (queue 15), now that the transform counter means something narrower
No mechanical gate on the untrusted boundary (ADR-0003) Scheduled to Arc 3: nothing untrusted is read yet. Half of it is mechanical in a different way since ADR-0060 — verify.sh requires html.WithUnsafe() in exactly one file, so a second pipeline trusting its input cannot appear unnoticed — but there is still no check that a future untrusted source stays out of shortcode and template evaluation The comment path — a test that untrusted input reaches no shortcode or template evaluation
A gallery's images carry no alt width/height now come from the original (ADR-0042), so only alt text is missing, and a filename does not supply one. An empty alt is honest for a picture the page has already introduced Captions per gallery entry — a sidecar or a frontmatter list — if the reference theme ever needs them
Sequence resolution rescans the index on every bundle request — two passes over every key, each doing a Lookup Measured at the same time as the pictures (ADR-0044): a whole page is ~63µs, so this is not what costs anything. Remembering it would be a cache with no measurement behind it A page render exceeding a few milliseconds, which is also what would revive the parked cache model
The root listing's <title> repeats itself — "A Khosra Demo · A Khosra Demo" Spotted 2026-08-01 by looking at the served page, not by any test: base.html joins page title and site title unconditionally, and at the root those are the same string. Cosmetic, and the fix is one if in a template — theme layer, not engine The next time base.html is edited for any reason — its old trigger named queue entry G4, which has been dropped
The theme and the index are two separate atomic.Pointer stores, so a request landing between them sees a new theme with the previous index Accepted 2026-08-01 with ADR-0056: both halves are internally coherent and the gap is microseconds, so no page is ever internally inconsistent — it is simply not a snapshot of the disk. Closing it means one pointer holding both, which changes web.Handler's signature and 20 test construction sites Anything that makes the gap observable — a request rate high enough to land in it, or a feature where content and theme must agree exactly (an export, where every page is generated in one pass)
Under the default include model, a fragment's footnotes render where the include sits, so a long one puts an <hr> and a numbered list mid-article Spotted 2026-08-01 by looking at the served page, not by any test. It is ADR-0038's documented consequence, and the ids are correctly namespaced (ADR-0058); only the placement reads badly. An author who minds now says include: merge (ADR-0066), which makes the page one document and puts every note at its end, so this is a default rather than a limit The default itself proving wrong — a site where every composed page sets the flag, at which point the flag is the wrong way round

Open questions

Subagents for fan-out reads — policy, not judgment. Delegating read-heavy sweeps (/audit, /refresh-docs, /invariants, rename sweeps) keeps thousands of lines of file dumps out of the main window and returns only the verdict. Deferred 2026-08-01; the case for and against is written out in ideas/token-conservation.md, including where it is clearly right (locating things) and clearly wrong (deciding things). Nothing blocks on it.

Nothing else blocks Arc 1 or the first deploy.

Every feature has been audited against the layer test (ADR-0046). One was wrong and was deleted (widow prevention, ADR-0045); one quietly decided presentation and now offers both shapes (tag listing grouping). The verdict table is in the ADR, and remaining queue entries carry a layer note before they are built.

Every ADR in decisions.md is accepted; none is open or proposed.

Build queue

Working plan lives in .scratch/build-queue.md, which is deliberately not committed — git log is the record of what actually landed. If that file is absent, read the log and rebuild the plan from it.

Arc retro log

One line per completed arc: what it cost, what it taught, what it made unnecessary.

  • Arc 1 — the spine. 619 core lines, 3 dependencies, 4 queue entries. Taught: os.Root makes the path guard a property of the type, so the latent item that shipped with the harness died instead of being implemented; and running the gates against real code found six defects in the gates — an allowlist parser that rejected its own documented format, two advisories that fired only on correct code, a coupling gate that demanded explanations for permission edits, an untidy go.mod hiding a direct dependency, and a nesting check off by one level. Made unnecessary: a hand-rolled traversal cleaner, and a second routing branch — the resolver arrived by counter at exactly the right moment.