Files
khosra/docs/state.md
T
bdeshi d80885a412 answer 404 for a static path the root refuses
Found by /invariants: a symlink under static/ pointing outside the site root
answered 500. The guard held — os.Root refused it and no bytes escaped — but the
response confirmed the path was there, where every other miss answers 404. Same
reasoning as a hidden bundle answering 404 rather than 403 (ADR-0024).

serveStatic now stats through the rooted FS first, so a directory, a missing
file, and a refused name are one answer. That also folds the old noListing and
staticFS into one function, since "cannot serve this" was already their shared
job.

The test uses a real temp directory rather than a MapFS, because the guard under
test belongs to os.Root; verified it fails with 500 against the previous code
before keeping it.

Splitting web_test.go at the seam the package already had — resolve_test.go for
what a path means, web_test.go for what happens once it resolves — because it
crossed FILE_LOC_WARN. Same response as content.go at entry 9.
2026-08-01 02:23:35 +06:00

7.0 KiB

State

Verified against: 26cc829 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, permalink building 352
internal/content/site.go the indexed site: lookup with language fallback, aliases, Query and Run, sections, Sequence 286
internal/render/render.go goldmark with the typographer, per-kind template sets with site override, Page/List/Sequence/head 305
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: base.html, page.html, list.html, theme.css (ADR-0026)
internal/web/resolve.go URL → (key, lang, page, tag) or a canonical redirect: language prefix, /en/… fork guard, pagination, tags, trailing slash 112
internal/web/web.go handler: resolve, look up with fallback, section and tag listings, sequence, /static/ (misses and refusals alike answer 404), degrade on failure 152
cmd/khosra/main.go flags, wiring, startup — the only place things are assembled 53
*_test.go table-driven, one file per source file; symlink escape (content and static), canonical paths, language fallback, aliases, pagination, tags, sequences, chrome, typography, 404 1172

Serves a bundle at /{section}/{slug}/, a paginated listing per section, tag listings global and section-narrowed, sequence navigation and a series archive on any nested bundle, and static/ verbatim. Chrome text, dates and digits render in English or Bengali; authored text is untouched but for typographic smoothing (ADR-0034). This repo holds engine source only — the site root is external and passed with -site (ADR-0011).

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

Dependencies: three, all allowlisted — goldmark, golang.org/x/text, 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 0 3 Stage pipeline (ordered func(ctx,*Page) error). Typography is not one: it is a goldmark parser option, not a function over a page, so it buys the feature without moving the counter. Shortcodes (queue 12) will be the first real one
Routing cases 5 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 / output formats 2 2 — due Two template sets exist (bundle, listing); the View layer is Arc 2's third item
Effects 0 2 Effect runner + trigger wiring (change / schedule / demand)
Extensions 0 3 Extension registry + wire file (extensions.md)
Interface implementations 2 The interface itself
Non-stdlib dependencies 3 direct budget in scripts/budgets.env

Allowlist, all three imported: goldmark (markdown), golang.org/x/text (NFC, ADR-0015), gopkg.in/yaml.v3 (frontmatter, ADR-0020).

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 3rd transform (queue 12)
No mechanical gate on the untrusted boundary (ADR-0003) Scheduled to Arc 3: nothing untrusted is read yet 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)
Sequence resolution rescans the index on every bundle request — two passes over every key, each doing a Lookup No cache exists anywhere yet, and a site of this size resolves in microseconds. Measuring first is the rule (queue 16) The page cache (queue 16), which is the thing that makes the cost visible
Raw HTML in Markdown is currently omitted only because goldmark's default omits it Verified at the Arc 1 boundary, and it is what keeps invariant 2 intact for authored content. html.WithUnsafe() is the obvious move when a shortcode needs to emit HTML, and it silently turns authored Markdown into an injection path Shortcodes (queue 12) — if unsafe rendering is enabled, the trusted/untrusted split must be real code, not a default

Open questions

None. Nothing blocks Arc 1 or the first deploy.

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.