A draft is now not served at all, and neither is a bundle whose date has not arrived. The filter sits in `Site.Lookup` and `Site.Run`, which is every path to a bundle — so the files inside an unpublished bundle inherit its status for free, which is what ADR-0024 asks for and what the asset route was written to allow. A test asserts the 404 for the bundle *and* its picture, and that nothing leaks into a listing, a feed or a sitemap. The clock is read per request rather than at startup, so a scheduled post appears exactly when its date arrives with nothing to restart and nothing to invalidate. That first clock read created internal/content/clock.go, which is the only place `verify.sh` allows `time.Now` — a render that depends on the time is worth being able to find. `-dev on` reveals both and reparses the theme before each render. Deliberately not a bare boolean flag: turning unpublished work into public work should not be one fumbled argument away. A reload that fails to parse leaves the working template set in place, so a typo shows an error rather than replacing a good set with a broken one.
112 lines
11 KiB
Markdown
112 lines
11 KiB
Markdown
# State
|
|
|
|
**Verified against:** `0231dae` 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/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 `Partial`/`Origin` seams features render and resolve through, `Page`/`List`/`Sequence`/`head` | 409 |
|
|
| `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`, `shortcodes.html`, `theme.css` (ADR-0026) | — |
|
|
| `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/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) or a canonical redirect: language prefix, `/en/…` fork guard, pagination, tags, trailing slash | 112 |
|
|
| `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: 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 (`-site`, `-addr`, `-base`, `-cache`, `-dev`), wiring, startup including the derivative pass — the only place things are assembled | 92 |
|
|
| `cmd/khosra/check.go` | the `check` subcommand: parse, print, exit code. What counts as a finding lives in the feature | 45 |
|
|
| `*_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 | 2608 |
|
|
|
|
Serves 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, 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. 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.
|
|
|
|
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 | 9 | **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 | 2 | **3** | Extension registry (`extensions.md`). Was briefly 3; deleting the widows feature (ADR-0045) put it back to 2, which is the counter doing its job — a threshold reached by a feature that should not have existed was not a threshold |
|
|
| Interface implementations | — | **2** | The interface itself |
|
|
| Non-stdlib dependencies | 4 direct | budget in `scripts/budgets.env` | — |
|
|
|
|
**When the Extensions counter comes due, look at how the features plug in.** The two that exist attach in
|
|
different ways — `shortcodes` is a goldmark extender in `extenders()`, `check` is a function `cmd` calls — so a
|
|
registry would have to abstract over "extends Markdown" and "validates content", which share nothing but the
|
|
word *feature*. Revisit when a third wants a *third* way in, or when one wants a route (the seam ADR-0042
|
|
named).
|
|
|
|
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 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.
|