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.
15 KiB
15 KiB
Surface
Generated by scripts/surface.sh — do not edit. Regenerate with make surface.
Every top-level declaration in the engine, with its line. Read this before opening a source
file: it answers "where does X live" and "what is in this package" without the bodies. What each
file is for lives in state.md; why it is that way lives in decisions.md.
cmd/khosra — 264 lines
check.go 45 · main.go 158 · new.go 42 · wire.go 19
- check.go:16 func runCheck(args []string)
- main.go:23 func main()
- main.go:44 func runServe()
- main.go:110 func rebuilder(fsys fs.FS, cache string, reveal bool, live *atomic.Pointer[content.Site],
- main.go:141 func defaultCache() string
- main.go:151 func fatal(msg string, err error)
- new.go:12 func runNew(args []string)
- wire.go:15 func extenders(partial render.Partial) []goldmark.Extender
internal/content — 1042 lines + 537 test
clock.go 12 · content.go 450 · doc.go 5 · extras.go 92 · settings.go 59 · site.go 424
- clock.go:9 var now = time.Now
- clock.go:12 func Now() time.Time { return now() }
- content.go:20 const DefaultLang = "en"
- content.go:27 type Bundle struct
- content.go:70 func OpenSite(dir string) (fs.FS, error)
- content.go:82 type Problem struct
- content.go:94 func Scan(fsys fs.FS) ([]Bundle, error)
- content.go:103 func ScanReport(fsys fs.FS) ([]Bundle, []Problem, error)
- content.go:142 func Parse(name string, data []byte) (Bundle, error)
- content.go:182 func (b Bundle) Published(at time.Time) bool
- content.go:191 func (b Bundle) Assets() (string, bool)
- content.go:201 func stringList(v any) []string
- content.go:223 func asTime(v any) time.Time
- content.go:239 func asInt(v any) int
- content.go:252 func terms(v any) []string
- content.go:276 func TagSlug(tag string) string
- content.go:285 func Normalise(s string) string { return norm.NFC.String(s) }
- content.go:291 func splitName(name string) (key, lang string, ok bool)
- content.go:313 func isLangTag(s string) bool
- content.go:331 func isPartial(base string) bool
- content.go:343 func skipDir(base string) bool
- content.go:351 func splitFrontmatter(data []byte) (front, body []byte)
- content.go:366 func trimLeadingFence(data []byte, fence string) ([]byte, bool)
- content.go:383 func dropCollisions(all []Bundle) ([]Bundle, []Problem)
- content.go:405 const PerPage = 10
- content.go:411 func URL(key, lang string) string
- content.go:424 func TagURL(section, slug, lang string, page int) string
- content.go:434 const DerivedPrefix = "/derived/"
- content.go:437 func DerivedURL(name string) string { return DerivedPrefix + name }
- content.go:440 const TagsSegment = "tags"
- content.go:444 func PageURL(key, lang string, page int) string
- extras.go:14 const ExtrasDir = "extras"
- extras.go:17 type Entry struct
- extras.go:33 func Extras(fsys fs.FS, b Bundle) []Entry
- extras.go:67 func KindOf(name string) string
- extras.go:86 func ExtrasURL(route, lang, entry string) string
- settings.go:13 const SettingsFile = "site.yaml"
- settings.go:20 type Settings struct
- settings.go:33 func LoadSettings(fsys fs.FS) (Settings, error)
- settings.go:54 func Absolute(base, path string) string
- site.go:11 type Site struct
- site.go:28 func NewSite(bundles []Bundle) *Site
- site.go:45 func (s *Site) Reveal() { s.reveal = true }
- site.go:52 func (s *Site) visible(b Bundle) bool
- site.go:58 func (s *Site) Problems() []Problem { return s.problems }
- site.go:61 func (s *Site) note(path, detail string)
- site.go:72 func (s *Site) indexRoutes(bundles []Bundle)
- site.go:111 func (s *Site) KeyFor(route string) (string, bool)
- site.go:122 func (s *Site) RouteOf(key string) string
- site.go:130 func sorted(set map[string]bool) []string
- site.go:143 func (s *Site) indexAliases(bundles []Bundle)
- site.go:175 func (s *Site) Alias(alias string) (string, bool)
- site.go:184 func (s *Site) Lookup(key, lang string) (b Bundle, served string, ok bool)
- site.go:202 func (s *Site) Variants(key string) []string
- site.go:216 func (s *Site) HasLang(lang string) bool
- site.go:226 func (s *Site) Len() int { return len(s.byKeyLang) }
- site.go:232 type Query struct
- site.go:243 func (s *Site) Run(q Query) []Bundle
- site.go:268 func (s *Site) keys() []string
- site.go:284 type Sequence struct
- site.go:304 func (s *Site) Sequence(key, lang string) (Sequence, bool)
- site.go:325 func (s *Site) parent(key string) (string, bool)
- site.go:335 func (s *Site) has(key string) bool
- site.go:344 func (s *Site) members(series, lang string) []Bundle
- site.go:369 func (b Bundle) hasTag(slug string) bool
- site.go:382 func (b Bundle) Section() string
- site.go:394 func (s *Site) Everything() []Bundle
- site.go:407 func (s *Site) Sections() []string
internal/ext/check — 223 lines + 147 test
check.go 215 · doc.go 8
- check.go:15 type Finding struct
- check.go:29 func Run(fsys fs.FS, bundles []content.Bundle, site *content.Site, problems []content.Problem) []Finding
- check.go:52 func Fatal(found []Finding) bool
- check.go:62 func inspect(fsys fs.FS, b content.Bundle, site *content.Site) []Finding
- check.go:78 var figureCall = regexp.MustCompile(
(?m)^\s*\{\{<\s*figure\s+([^>]*)>\}\}\s*$) - check.go:81 var altArg = regexp.MustCompile(
alt="[^"]+") - check.go:87 func checkFigures(b content.Bundle) []Finding
- check.go:98 var internalLink = regexp.MustCompile(
\]\((/[^)\s"]*)) - check.go:104 func checkLinks(fsys fs.FS, b content.Bundle, site *content.Site) []Finding
- check.go:121 func engineOwned(target string) bool
- check.go:138 func resolves(fsys fs.FS, target string, from content.Bundle, site *content.Site) bool
- check.go:164 func asset(fsys fs.FS, trimmed string, site *content.Site) bool
- check.go:192 func mixedOrdering(bundles []content.Bundle, site *content.Site) []Finding
internal/ext/scaffold — 102 lines + 89 test
doc.go 8 · scaffold.go 94
- scaffold.go:23 func New(siteDir, key, lang, title string) (string, error)
- scaffold.go:69 func frontmatter(title string) string
- scaffold.go:76 func titleFrom(key string) string
- scaffold.go:85 func mkdirAll(root *os.Root, dir string) error
internal/ext/shortcodes — 569 lines + 417 test
doc.go 7 · images.go 250 · shortcodes.go 312
- images.go:31 var widths = []int{480, 960, 1440}
- images.go:38 func Derive(siteFS fs.FS, cacheDir string) (int, error)
- images.go:76 func derive(data []byte, name, cacheDir string) (int, error)
- images.go:103 func scale(src image.Image, width int) image.Image
- images.go:115 func write(file string, img image.Image, source string) error
- images.go:133 func encode(w *os.File, img image.Image, source string) error
- images.go:149 var
- images.go:159 func picture(origin render.Origin, file string) (render.Picture, bool)
- images.go:214 func derivedName(data []byte, source string, width int) string
- images.go:224 func lossless(source string) bool
- images.go:234 func showable(name string) bool
- images.go:244 func derivable(name string) bool
- shortcodes.go:24 const
- shortcodes.go:33 func New(partial render.Partial) goldmark.Extender
- shortcodes.go:37 type extension struct
- shortcodes.go:46 func (e extension) Extend(md goldmark.Markdown)
- shortcodes.go:56 var nested = parser.NewContextKey()
- shortcodes.go:64 type includes struct
- shortcodes.go:68 func (in includes) Transform(doc *ast.Document, reader text.Reader, pc parser.Context)
- shortcodes.go:88 func (in includes) convert(name string, pc parser.Context) ([]byte, error)
- shortcodes.go:118 func pending(doc *ast.Document) []*node
- shortcodes.go:136 var kind = ast.NewNodeKind("Shortcode")
- shortcodes.go:139 type node struct
- shortcodes.go:153 func (n *node) Kind() ast.NodeKind { return kind }
- shortcodes.go:155 func (n *node) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) }
- shortcodes.go:158 type blocks struct{}
- shortcodes.go:160 func (blocks) Trigger() []byte { return []byte{'{'} }
- shortcodes.go:162 func (blocks) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State)
- shortcodes.go:193 func gallery(pc parser.Context) []render.Picture
- shortcodes.go:220 func (blocks) Continue(n ast.Node, reader text.Reader, pc parser.Context) parser.State
- shortcodes.go:224 func (blocks) Close(n ast.Node, reader text.Reader, pc parser.Context) {}
- shortcodes.go:226 func (blocks) CanInterruptParagraph() bool { return true }
- shortcodes.go:228 func (blocks) CanAcceptIndentedLine() bool { return false }
- shortcodes.go:231 type fragments struct
- shortcodes.go:235 func (f fragments) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer)
- shortcodes.go:243 func (f fragments) render(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error)
- shortcodes.go:270 func parse(line string) (name string, args map[string]string, ok bool)
- shortcodes.go:297 func argument(s string) (key, value, rest string, ok bool)
internal/ext/watch — 136 lines + 114 test
doc.go 8 · watch.go 128
- watch.go:23 var
- watch.go:29 type Changed func()
- watch.go:35 func Watch(fsys fs.FS, stop <-chan struct{}, onChange Changed)
- watch.go:79 func Fingerprint(fsys fs.FS) string
- watch.go:93 func record(sum hash.Hash, p string, d fs.DirEntry, err error) error
- watch.go:116 func dropping(name string) bool
internal/render — 726 lines + 407 test
chrome.go 110 · render.go 486 · view.go 130
- chrome.go:19 var chrome = map[string]map[string]string{
- chrome.go:33 var months = map[string][]string{
- chrome.go:41 var digits = map[string][]rune{
- chrome.go:47 var funcs = template.FuncMap{"t" text, "num" numerals, "day" day}
- chrome.go:53 func text(lang, key string, args ...string) string
- chrome.go:73 func numerals(lang string, n int) string
- chrome.go:82 func day(lang string, t time.Time) string
- chrome.go:98 func localiseDigits(lang, s string) string
- render.go:25 var themeFS embed.FS
- render.go:29 type Renderer struct
- render.go:52 type parsedTheme struct
- render.go:66 type Partial func(name string, data Fragment) ([]byte, error)
- render.go:69 type Fragment struct
- render.go:79 type Picture struct
- render.go:96 type Origin struct
- render.go:105 var originKey = parser.NewContextKey()
- render.go:108 func OriginFrom(pc parser.Context) (Origin, bool)
- render.go:115 func WithOrigin(pc parser.Context, origin Origin)
- render.go:128 func New(siteFS fs.FS, settings content.Settings, extend func(Partial) []goldmark.Extender) (*Renderer, error)
- render.go:154 func parseTheme(siteFS fs.FS) (*parsedTheme, error)
- render.go:182 func (r *Renderer) head(title, lang, canonical string) head
- render.go:199 func (r *Renderer) absolute(path string) string
- render.go:207 func (r *Renderer) Navigation(sections func() []string) { r.sections = sections }
- render.go:211 func (r *Renderer) Reload() { r.reload = true }
- render.go:218 func (r *Renderer) Refresh() error
- render.go:230 func (r *Renderer) fresh() error
- render.go:239 func (r *Renderer) Partial(name string, data Fragment) ([]byte, error)
- render.go:258 func parseSet(siteFS fs.FS, names ...string) (*template.Template, error)
- render.go:280 func readStyle(siteFS fs.FS) (template.CSS, error)
- render.go:298 func (r *Renderer) Extras(b content.Bundle, served string, entries []content.Entry, selected *Selected) ([]byte, error)
- render.go:322 func (r *Renderer) RenderText(kind string, data []byte) (template.HTML, error)
- render.go:340 func (r *Renderer) Bundle(b content.Bundle, served string, variants []string, seq *content.Sequence) ([]byte, error)
- render.go:377 func (r *Renderer) Listing(section, lang string, all []content.Bundle, page int) ([]byte, error)
- render.go:399 func (r *Renderer) Tag(section, slug, lang string, all []content.Bundle, page int) ([]byte, error)
- render.go:426 func (r *Renderer) sequence(seq *content.Sequence, lang string) *Sequence
- render.go:453 func (r *Renderer) item(b content.Bundle, lang string) Item
- render.go:458 func (r *Renderer) paginate(title, lang, canonical string, all []content.Bundle, page int, url func(int) string) (List, []content.Bundle)
- render.go:480 func (r *Renderer) execute(set *template.Template, data any, what string) ([]byte, error)
- view.go:16 type head struct
- view.go:36 type Page struct
- view.go:55 type Sequence struct
- view.go:71 type Extras struct
- view.go:82 type Selected struct
- view.go:91 type List struct
- view.go:105 type Group struct
- view.go:111 type Item struct
- view.go:122 type Alternate struct
internal/web — 734 lines + 1586 test
asset.go 58 · discover.go 71 · extras.go 93 · feed.go 125 · resolve.go 170 · web.go 217
- asset.go:22 func serveAsset(w http.ResponseWriter, req *http.Request, site *content.Site, siteFS fs.FS, res resolution) bool
- discover.go:14 const
- discover.go:24 func serveRobots(w http.ResponseWriter, req *http.Request, siteFS fs.FS, base string)
- discover.go:45 func serveSitemap(w http.ResponseWriter, req *http.Request, site *content.Site, base string)
- discover.go:67 func xmlEscape(s string) string
- extras.go:18 func serveExtras(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer,
- extras.go:69 func renderExtras(w http.ResponseWriter, r *render.Renderer, b content.Bundle, served string,
- extras.go:85 func find(entries []content.Entry, want string) (content.Entry, bool)
- feed.go:13 const feedFile = "feed.xml"
- feed.go:17 const feedMax = 20
- feed.go:24 type atom struct
- feed.go:34 type atomLink struct
- feed.go:40 type atomEntry struct
- feed.go:52 func serveFeed(w http.ResponseWriter, req *http.Request, site *content.Site, res resolution, settings content.Settings) bool
- feed.go:102 func dated(all []content.Bundle) []content.Bundle
- feed.go:113 func feedTitle(settings content.Settings, res resolution) string
- resolve.go:12 type resolution struct
- resolve.go:40 func resolve(path string, site *content.Site) (resolution, bool)
- resolve.go:102 func cutLang(key string, site *content.Site) (lang, rest, redirect string)
- resolve.go:117 func cutExtras(key string) (bundle, entry string, ok bool)
- resolve.go:131 func cutFeed(key string) (rest string, ok bool)
- resolve.go:142 func cutTag(key string) (tag, section string, ok bool)
- resolve.go:154 func cutPage(key string) (rest string, page int, ok bool)
- web.go:19 type Current func() *content.Site
- web.go:22 func Fixed(site *content.Site) Current { return func() *content.Site { return site } }
- web.go:27 func Handler(current Current, r *render.Renderer, siteFS, derivedFS fs.FS, settings content.Settings) http.Handler
- web.go:60 func serveStatic(sub fs.FS) http.Handler
- web.go:76 func serveListing(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer, res resolution) bool
- web.go:100 func serveTags(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer, res resolution) bool
- web.go:125 func write(w http.ResponseWriter, out []byte, what string)
- web.go:134 func writeAs(w http.ResponseWriter, contentType string, out []byte, what string)
- web.go:142 func serve(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer, siteFS fs.FS, settings content.Settings)
- web.go:173 func serveBundle(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer,