Files
khosra/docs/state.md
T
Claude Opus 5andbdeshi 04ae2ff475 prevent widows, as the second feature package
The last two words of a paragraph or heading are joined by a non-breaking space,
so one word never falls alone onto its own line. Deferred from entry 11 for the
right reason: over rendered HTML this cannot tell prose from an escaped code span,
so it had to wait for a tree transform.

The interesting failure is worth keeping: written against goldmark alone it passed
six tests, and did nothing in the real engine. The typographer splits a text run
wherever it looks for a substitution, so a paragraph ending "hand." arrives as two
text nodes and the last of them holds no space at all. A version that inspects
only the last child therefore finds nothing to join. It now takes the whole
trailing run of text nodes, stopping at a line break or any markup, and there is a
regression test that builds both extensions together — the only configuration that
would have caught it.

The joined text becomes a String node, which carries its own bytes: a segment is
an offset into bytes every node shares, so editing the source in place is not
possible. That path still escapes, and a test says so, since otherwise this
transform would be an injection route.

PhaseMarkup is now empty in extensions.md, and honestly so: everything expected
there turned out to belong either earlier or later.
2026-07-31 02:12:08 +06:00

92 lines
8.2 KiB
Markdown

# State
**Verified against:** `ff62729` 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 | 368 |
| `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, the `Partial`/`Origin` seams features render and resolve through, `Page`/`List`/`Sequence`/`head` | 386 |
| `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` | 315 |
| `internal/ext/widows/` | second feature: joins the last two words of a paragraph or heading with a non-breaking space, over the tree so code spans are safe | 108 |
| `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/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, shortcode escaping, galleries, includes, partials, widows, 404 | 1562 |
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 and widow prevention (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 — **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, shortcodes and widows 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 | 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 | 2 | **3** | Extension registry (`extensions.md`). The wire file arrived with the first feature rather than the registry — `cmd/khosra/wire.go`, one line, no struct |
| 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 | 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) |
| The reference theme's images carry no `width`/`height`, and a gallery's carry no `alt` — below the output floor `conventions.md` states | Nothing can supply them yet: dimensions need the image read, and a filename is not alt text. An empty `alt` is at least honest about a picture nothing describes | Image derivatives (queue 13) compute dimensions; structured gallery items with captions land with them (ADR-0037's revisit note) |
| 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 |
## 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.