apply a template edit without a restart

The watcher fingerprinted templates/ but a rebuild only re-scanned content, so
editing a template fired a rebuild that changed nothing. ADR-0022 already
promised the opposite — "a template edit in the site root invalidates through
the same path as content" — which makes this a defect against a recorded
decision rather than a missing feature. ADR-0055 records the fix and supersedes
ADR-0048's narrower clause.

The parsed sets and the stylesheet become one parsedTheme behind an
atomic.Pointer, swapped by Refresh once per rebuild instead of per request. A
parse failure keeps the theme that was working, so a typo cannot take the site
down. The swap also retires the in-place field mutation -dev was doing, which
was a data race with every in-flight render.

site.yaml goes the other way and leaves the fingerprint: the settings are copied
by value into the renderer, the handler, the feeds and the sitemap, so applying
a change to some of them is worse than applying it to none. It is restart-only.

Corrects the Effects counter row while proving it did not move: it still said
startup was the only change signal "until queue 21", but queue 21 shipped as
ADR-0048 and put the derivative pass inside rebuilder, so that has been wrong
since. The row now also answers the question ADR-0055 invites — an in-memory
swap is not an Effect, because it writes no artifact and calls nothing outbound.

Measured on the real binary: a template edit went live in ~2s; a typo logged
"keeping the previous theme" and kept answering 200 with the last good markup; a
site.yaml edit now fires no rebuild at all. core 2766/2800, ext 1030/2000,
34 gates green, 0 warnings.
This commit is contained in:
Claude Opus 5
2026-08-01 10:54:30 +06:00
committed by bdeshi
parent 36194a16d8
commit 633debf743
10 changed files with 246 additions and 109 deletions
+4 -5
View File
@@ -72,6 +72,10 @@ func Watch(fsys fs.FS, stop <-chan struct{}, onChange Changed) {
// Names, sizes and modification times — not contents: reading every file to detect a change would cost more
// than the rebuild it triggers. Editor droppings are excluded, or saving a file in vim would look like three
// changes and a deletion.
//
// `content/` and `templates/` only. `site.yaml` is deliberately absent: a rebuild cannot apply it, since the
// settings are copied by value into the renderer, the handler, the feeds and the sitemap, and a fingerprint
// that fires a rebuild changing nothing is a lie told every two seconds (ADR-0055).
func Fingerprint(fsys fs.FS) string {
sum := sha256.New()
for _, root := range []string{"content", "templates"} {
@@ -79,11 +83,6 @@ func Fingerprint(fsys fs.FS) string {
return record(sum, p, d, err)
})
}
// site.yaml is part of the site's state, and editing it should not need a restart.
if info, err := fs.Stat(fsys, "site.yaml"); err == nil {
sum.Write([]byte("site.yaml"))
_ = binary.Write(sum, binary.LittleEndian, info.ModTime().UnixNano())
}
return hex.EncodeToString(sum.Sum(nil))
}
+12 -1
View File
@@ -25,7 +25,6 @@ func TestFingerprintChangesOnlyForContent(t *testing.T) {
"edited a bundle": {"content/posts/one.md": {Data: []byte("xx"), ModTime: time.Unix(200, 0)}},
"added a bundle": {"content/posts/two.md": {Data: []byte("z"), ModTime: time.Unix(100, 0)}},
"edited a template": {"templates/page.html": {Data: []byte("yy"), ModTime: time.Unix(200, 0)}},
"edited site.yaml": {"site.yaml": {Data: []byte("base: y"), ModTime: time.Unix(200, 0)}},
} {
next := fstest.MapFS{}
for k, v := range base {
@@ -39,6 +38,18 @@ func TestFingerprintChangesOnlyForContent(t *testing.T) {
}
}
// site.yaml is deliberately unwatched: a rebuild cannot apply it, because the settings are copied by value
// into the renderer, the handler, the feeds and the sitemap. Noticing it would fire a rebuild that changes
// nothing (ADR-0055), so editing it needs a restart.
settings := fstest.MapFS{}
for k, v := range base {
settings[k] = v
}
settings["site.yaml"] = &fstest.MapFile{Data: []byte("base: y"), ModTime: time.Unix(200, 0)}
if Fingerprint(settings) != before {
t.Error("site.yaml changed the fingerprint, so a rebuild will fire and apply nothing")
}
// Editor droppings are not content: saving in vim writes several of these, and each would look like a change.
noise := fstest.MapFS{}
for k, v := range base {