delete the widows feature; line breaking is CSS

The human asked whether widow prevention belonged in the backend at all. It did
not, and it broke two rules already written down: the theme contract says the
engine decides nothing about how something looks, and ADR-0034 says authored body
text is the author's — while this inserted U+00A0 into that text.

The practical harm follows from the layer error rather than from a coding mistake.
The engine cannot see the line box, so joining the last two words is a guess that
can overflow a narrow viewport, and a reader copying the paragraph gets a
non-breaking space in their clipboard. `text-wrap: pretty` and `text-wrap: balance`
in the reference stylesheet know the line box and need no bytes in the content.

108 lines of engine deleted for one CSS declaration. The typographer stays: turning
`--` into an en dash is a text transformation no stylesheet can express, which is
exactly the distinction the new layer test draws.

Also worth recording: this took the Extensions counter from 3 back to 2. A threshold
reached by a feature that should not have existed was never a threshold.
This commit is contained in:
2026-07-31 12:47:03 +06:00
parent cf398af725
commit 12f9a31779
10 changed files with 43 additions and 222 deletions
+9 -11
View File
@@ -17,7 +17,6 @@ If this file disagrees with the code, the code is right and this file is a bug.
| `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 |
| `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/asset.go` | files inside a bundle's own directory, looked up through the owning bundle so visibility can only ever inherit (ADR-0024) | 58 |
@@ -26,7 +25,7 @@ If this file disagrees with the code, the code is right and this file is a bug.
| `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`), 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, widows, site settings, absolute URLs, robots, sitemap, slug routes, bundle assets, derivatives, feeds, 404, plus benchmarks for the render path and the checker | 2604 |
| `*_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 | 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
@@ -34,7 +33,7 @@ section-narrowed, sequence navigation and a series archive on any nested bundle,
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 and widow prevention (ADR-0034). This repo holds engine source only — the site root is external and passed with
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.
`-site` (ADR-0011). `site.yaml` declares `base` and `title`; with a base, canonical, hreflang and OpenGraph
URLs go absolute (ADR-0039).
@@ -52,21 +51,20 @@ 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 |
| 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 | 3 | **3** — due | Extension registry (`extensions.md`). Three features exist, so the threshold is reached: see the note below the table before building one | Extension registry (`extensions.md`). The wire file arrived with the first feature rather than the registry — `cmd/khosra/wire.go`, one line, no struct |
| 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` | — |
**Extensions counter is due, and the answer is probably still no.** Three features exist
(`shortcodes`, `widows`, `check`) but they plug in three different ways: two are goldmark extenders listed in
`extenders()`, and `check` is a function `cmd` calls. A registry would have to abstract over "thing that
extends Markdown" and "thing that validates content", which share nothing but the word *feature*. What the
counter is really detecting is that `wire.go` lists only one kind. Revisit when a *fourth* feature wants a
third way in — or when one wants a route, which is the seam ADR-0042 already named.
**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).