Files
khosra/docs/state.md
T
bdeshi 8900aabc57 track the demo as a real site in examples/, gated like the docs
The human asked for a demo extensive enough to review by hand, tracked as files,
and kept current the way docs are. Generated filler cannot be reviewed — you cannot
read a Go function and see what a reader sees — so the generator is deleted and
`examples/demo-site/` is an ordinary site: 33 Markdown files, six pictures,
site.yaml, a template override and static files.

My reason for generating it was ADR-0011, and I had that rule wrong: it puts the
*author's site root* outside this repository, not fixtures. conventions.md already
keeps golden files in testdata/, and this is the same category one size up. Keeping
both a generator and files would have been two sources of truth, so one had to go.

Kept true by two gates rather than by good intentions. A table-driven test in
internal/web serves the directory through the real handler with one case per
feature — thirty-five of them, each naming what it proves — and verify.sh runs
`khosra check` over it and fails on anything fatal. Adding a feature now means
adding it to the demo and adding its case, and the build says so if you don't.

Extensive on purpose: eleven dated posts so a section actually paginates, a
four-chapter series so first/last are not the neighbours, a gallery with three
JPEGs and an SVG so both the resampled and the untouched paths show, a Bengali-only
bundle, a titleless status, a draft, a future date, an include, a nested extras
tree, and a typography page that demonstrates what the engine will not do.

Two expectations of mine were wrong and the demo corrected them: the site's own
list template overrides *tag* listings too, so there are no group headings there —
which turns out to be the better demonstration of ADR-0046, since the theme picking
the flat shape is exactly the point. And template literal text is not escaped, so
an apostrophe stays an apostrophe.
2026-08-01 02:23:37 +06:00

14 KiB

State

Verified against: 8a2ef08 on 2026-07-30 — update this line every change. If this file disagrees with the code, the code is right and this file is a bug.

Inventory

File Purpose LOC
go.mod module khosra; goldmark, x/text, yaml.v3 direct 10
internal/content/doc.go package comment 5
internal/content/content.go bundles: os.Root open, walk, frontmatter split, key/lang derivation, NFC, tag slugs, partial files, permalink building 381
internal/content/clock.go the one place the engine reads the wall clock, which verify.sh enforces by filename 12
internal/content/extras.go a bundle's supporting files: enumeration, classification, and their URLs (ADR-0047) 96
internal/content/settings.go site.yaml: the site's own declarations (base, title) and absolute-URL building (ADR-0039) 59
internal/content/site.go the indexed site: lookup with language fallback, aliases, Query and Run, sections, Sequence, Everything, slug routes, publication visibility 424
internal/render/render.go goldmark with the typographer, per-kind template sets with site override, the render methods 447
internal/render/view.go the theme contract in Go: Page, List, Sequence, Extras, Item, Fragment, Picture, Origin 130
internal/render/chrome.go the engine's own words: phrase table, month names, digits, and the t/num/day template funcs (ADR-0034) 105
internal/render/templates/ reference theme, complete: base.html (shell, navigation, language links, feed and OpenGraph), page.html (bundle, sequence, tags, extras), list.html, extras.html, shortcodes.html, 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 (ADR-0042, ADR-0044) 564
internal/ext/scaffold/ writes one draft directory bundle into a site root through os.Root: never an overwrite 102
internal/ext/watch/ polls the site root, ignores editor droppings, and reports a settled change (ADR-0022, ADR-0048) 129
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 216
cmd/khosra/wire.go the only list of enabled features (extensions.md) 20
internal/web/resolve.go URL → (key, lang, page, tag, feed, extras) or a canonical redirect 168
internal/web/extras.go the extras route: listing, one entry selected, or ?raw bytes, all behind the bundle lookup 96
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) 58
internal/web/feed.go Atom for the site, a section or a tag, from dated bundles via one Query (ADR-0043) 125
internal/web/discover.go /robots.txt and /sitemap.xml, absolute and only with a declared base (ADR-0039) 74
internal/web/web.go handler: serve dispatches by kind, serveBundle answers the commonest one; listings, /static/, /derived/, degrade on failure 206
cmd/khosra/main.go flags, wiring, startup, the derivative pass, and the atomic swap a rebuild goes through. main dispatches subcommands, runServe assembles the server, rebuilder is used at startup and on every change alike 150
cmd/khosra/check.go the check subcommand: parse, print, exit code. What counts as a finding lives in the feature 45
cmd/khosra/new.go the new subcommand: arguments in either order, then the feature does the writing 42
*_test.go table-driven, one file per source file; 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 3233

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. 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 content changes by polling and swaps the index atomically, so an edit appears without a restart (ADR-0022). 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 reloads templates per request. -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. 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.

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

Dependencies: four, all allowlisted — goldmark, golang.org/x/text, golang.org/x/image, gopkg.in/yaml.v3.

Counters — the earn-it authority

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

Counter Now Extraction due at What it buys
Render transforms — page-level only 0 3 Stage pipeline (ordered func(ctx,*Page) error). Parse-phase work does not count and must not: goldmark's extender list is already an ordered pipeline for it, so typography and shortcodes compose there (cmd/khosra/wire.go) and a second pipeline beside it would be pure duplication. This counts transforms over the assembled page, which nothing hosts yet — OpenGraph and JSON-LD (queue 15) are the first candidates
Routing cases 11 2 — done Resolver at internal/web/resolve.go: bundle, language prefix, pagination, tag, section-narrowed tag
Collection pages 4 1 — done Query primitive: content.Query{Section, Tag, Lang} + Site.Run. The fourth — a series archive — resolves through Site.Sequence instead: membership is structural and the sort ascends, so it shares 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 are counted separately and are not it: 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 first is the derivative pass (ADR-0042), called straight from cmd at startup — one call needs no runner, and startup is the only change signal until queue 21
Extensions 3 3 — due, 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. It is 3 again with scaffold, and the note below the table says why a registry still buys nothing
Interface implementations 2 The interface itself
Non-stdlib dependencies 4 direct budget in scripts/budgets.env

The Extensions counter is due, and a registry would still buy nothing. The three features attach in two unrelated ways: shortcodes is a goldmark extender listed in extenders(), while check and scaffold are functions cmd calls for a subcommand. A registry would have to abstract over "extends Markdown", "validates content" and "writes a file", which share nothing but the word feature — one member and no leverage. What the counter is really detecting is that two of the three are commands, and commands compose fine as a switch in main. Build the registry when a feature wants a route (the seam ADR-0042 named) or when two features need to agree on an order.

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 now mechanical — verify.sh rejects WithUnsafe, so authored Markdown cannot become HTML — 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
date stays in Extra after being lifted onto Bundle.Date, unlike title, aliases, tags and order, which are deleted Spotted while adding order; the theme contract says Extra holds what the parser does not name, so one of the two is wrong. Harmless today — a template reading .Extra.date gets the raw YAML value Whatever next reads Extra generically: feeds (queue 14) or check (17)
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 picture memo is never evicted — one entry per picture on the site, for the life of the process Correct for one author's site, and the alternative is an eviction policy nothing needs. It is keyed on size and modification time, so it cannot go stale, only grow A site root large enough that memory matters, or a long-running process where pictures churn

Open questions

None. Nothing 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.