| type | reference | |||||
|---|---|---|---|---|---|---|
| links |
|
Deferred work and accepted limitations, with enough context to pick up later. Not bugs that block use — conscious deferrals.
-
compositor.tomlchanges take effect only on restart. Theservewatcher captures the loaded config by value, so an edit (e.g.site_name) triggers a rebuild that still uses the startup config. Fine for content editing; revisit if config becomes something you tune live. -
A
.gitignoreoutside the watched paths does not trigger a rebuild. The watcher covers the docs tree plus the project dir's own entries, so the usual repo-root.gitignoredoes; one in a directory between the project dir and a nesteddocs_dir, or in a repo root above the project dir, does not — its rules land on the next rebuild, or on restart. TheExcluderis rebuilt every cycle (seerebuild_into), so this is staleness of the trigger, not of the rules. Revisit by watching the collected.gitignorepaths. -
A repo-root image added after startup under a subdirectory 404s until another edit triggers a rebuild. The project-dir watch is non-recursive, so a README/CLAUDE page's image in, say,
images/(seeroot_assets::RootAssets) lands unseen.serve's lenient policy renders a not-yet-existing one as an honest 404, and the next rebuild resolves it. Correct-by-design (honest 404, not a stale freeze), not a bug. Revisit alongside the.gitignoreentry if repo-root assets ever need their own watch. -
A panic while handling a request kills that server, silently. No reachable panic path from external input exists today (the lock's critical sections are panic-free, so it can't poison; the only other
expects are two static, always-valid headers), so this is latent, not live. What changed withserve_handleis the blast radius, and it now differs per entry point:run_serve(CLI): the loop is the main thread — a panic aborts the process. Loud.serve_handle(embedded): the loop is one background thread among N. A panic takes down that one site's server while the host still believes it is live. Silent, and the host cannot see it without checking. A host embedding this must detect a dead serve thread rather than trust the handle (lector drops itslivedot on one). Defense-in-depth here: isolate each request (catch_unwind).
-
The 404 page carries no live-reload script. A tab showing a dead link's 404 will not auto-reload when the missing file is later created (the linking page does reload, since it is a real injected page). Minor UX asymmetry, consistent with the honest-404 design. Could inject the reload script (baselined at the current epoch) into the 404 body if wanted.
-
Single-threaded request loop can head-of-line block.
tiny_http'sincoming_requests()is a single consumer andrespond()writes synchronously, so a slow-reading client briefly blocks other tabs' reload polls. Acceptable ceiling for a local/loopback server; revisit only ifserveis put in front of many concurrent real readers. Underserve_handlethis is a non-issue by construction for cross-site blocking: one server per site, not one shared, so a slow reader on one site cannot block another's reload poll. The same synchronous write does giveserve_handlea new consequence, though: unbounded shutdown latency.ServeHandle::stop()joins the rebuild thread first (which may be mid-rebuild on a large tree), thenunblock()s and joinsserve_thread— butunblock()only releases a thread parked inrecv(); it cannot interrupt an in-flightrespond(), which is synchronous. A client that requests a large asset and stops reading holdsshutdown()/Dropuntil that write unsticks. Neither wait is bounded, though both finish given a cooperative client and a finite rebuild; low probability on loopback with small payloads, but matters most to a host callingDropon a UI thread. -
rewrite_link(render-core/src/markdown.rs) never percent-decodes and only ever splits#, never?. A link to a real file with a space —[link](my%20page.md)pointing atmy page.md— hard-fails a strict build as an unresolvable link, even though the file exists. The image path one function away does handle it:resolve_imagein the same module splits#/?off an image url and percent-decodes the remaining path before resolving, and is the model to follow if this is ever fixed. Fixing it would converge the two split sites, at which point a shared(path, suffix)helper would earn its keep — today they genuinely diverge (rewrite_linkalso does.md->.html), so they're deliberately not collapsed.
-
The generated
/index and the promoted docs-root/-alias are read-only. A synthesized generated index (generated_index) has no backing file to write. A promoted docs-roothome/readme(resolve_hometier 2) is served at/as a copy of a page that is already inline-editable at its own url, so the/alias is deliberately left read-only — edit the page at its own url. (The repo-root README/CLAUDE/AGENTS pages, and a docs-rootindex.mdserved at/directly, are inline-editable.) -
Admonitions and wikilink-dense blocks are not inline-editable (
data-noedit).preprocess_admonitionsrewrites!!!blocks before comrak parses, so comrak's source positions inside an admonition point at the injected<div>, not the original!!!lines — faithful source-mapping there needs the preprocessor taught to carry the original ranges (the passthrough line-map inpreprocess_admonitions_mappedhandles only the surrounding non-admonition lines, which is exactly what marks an admonition-touched block read-only). Deferred as its own piece of work. -
Structural editing is bounded to edit / add-adjacent / delete within a dirty region. Reconstruction range-replaces a dirty region (consecutive edited blocks bounded by untouched mapped blocks), so a paragraph split off with Enter, or a deleted block, round- trips. Not covered: drag-reorder across blocks, and a nested list inside an
<li>is flattened when its parent list is edited. Confirm the live DOM shape before extending. -
Editing is confined to existing files — no create / rename / delete from the browser.
/__editonly writes a url already in theeditablemap; adding, moving, or removing a page is out of scope for v1. -
The JS harness covers the pure serializer helpers only.
crates/compositor/tests/editor.test.cjs(Node, no DOM) exercises whateditor.jsexports under Node — inline and heading serialization. Reconstruction (range-replacement over the live#doc) needs a DOM and stays covered by the Rust/__editendpoint tests plus an end-to-end headless-browser pass (edit a block on a page carrying an admonition + code + frontmatter + task list, thendiffthe file to prove only the edited lines changed). Export more helpers into the harness as they become pure. -
/__editdoes not drain an oversized body on a kept-alive connection. The handler caps the body it buffers (MAX_EDIT_BODY) and rejects an over-cap request400, but on a keep-alive socket the un-consumed remainder is never drained, so a later request on the same socket could desync. Blast radius is the one misbehaving connection, and the endpoint is loopback/editor-only; drain-and-discard the remainder if it ever matters.
-
RootAssets'sRewritecan emit a raw#/?that came from the filename, not the author's markup.RootAssetsreturns a decoded filesystem path, soresolve_image'sformat!("{u}{suffix}")can splice a literal#/?from a filename into the emittedsrc. A repo-root README writingfor a real file namedfoo#bar.pngemitssrc="foo#bar.png"; the browser reads#bar.pngas a fragment and 404s, even though the file is copied tosite/. Comrak re-encodes a space in the common case, so this is narrow: only a literal#or?inside a filename triggers it. OnlyRewriteis affected, and onlyRootAssetsreturnsRewrite—DocsAssetsreturnsKeep, untouched. Without this resolution path such an image would not resolve at all, so it is a 404 either way for a pathological filename — an incomplete edge, not a regression. Fixing it needs percent-encoding against a correct path character set, which is easy to get subtly wrong, to serve a filename most sites will never have. Degrades visibly (a 404, not silent data loss) and the file is still copied. Revisit if a real site hits it. -
is_gitignoredonly sees the.gitignorefilesExcluder::newcollected.collect_gitignoresgathers the repo root, each directory between it and the docs dir, and each directory beneath the docs dir — deliberately not every directory in the repo (that walkstarget/, which Cargo seeds with a.gitignorecontaining*, and pruning would need the matchers still being built). Sois_gitignored— which judges assets outside the docs tree forRootAssets— consults the repo-root.gitignore(the one that hides scratch in practice) but not a.gitignoresitting in an outside-docs directory, e.g. a repo-rootimages/.gitignore. An image only that file ignores would be copied into the site. Narrow: it needs a repo-root README to reference an image that a nested, outside-docs.gitignorehides. Fixing it means collecting matchers lazily per queried path, which buys a second resolution path for a case nobody has hit. Revisit if one shows up.
- No shortest-unique-suffix path matching. Path-qualified
[[dir/Name]]matches the exact rel-path stem only. Obsidian's shortest-unique-suffix resolution ([[Name]]matchinga/b/Namewhen unique) is not implemented; use the full path from the docs root, or a title/alias. - No embeds / transclusion.
![[Name]]is not supported — comrak's wikilink extension does not fire on the!-prefixed form (the!is consumed by the image-opening logic first), so it passes through as inert literal text (![[Name]]), never resolving as a link or image. Deferred; add if a consuming KB needs inline embeds. - Anchors are not validated.
[[Name#section]]appends#sectionto the href without checking the target page actually has that heading id — consistent with the existing.md#fragpassthrough. A wrong anchor lands on the page top, not an error.
- A link from a kept page into an excluded subtree is an unresolvable link.
An excluded page is dropped from the known-URL set entirely, so a link to it
from a page that is rendered has nothing to resolve against — under a
strict
buildthat's a hard error, same as any other broken link (--lenientrenders it as a 404 instead). By design, not a bug: during a migration, either pass--lenientor remove the link.
-
A TOC link to a heading inside a collapsed
???doesn't auto-expand it. Headings inside an admonition are included in the right-rail TOC (matching MkDocs' inclusion behavior). But when the heading lives inside a collapsed???/???admonition, its<details>renders closed, so clicking the TOC entry scrolls to an anchor inside hidden content and nothing visibly happens until the reader expands the block. MkDocs Material adds JS that opens the<details>on anchor navigation; compositor does not yet. Low-likelihood (h2/h3 inside a callout is rare) UX papercut; add the open-on-anchor script tocompositor.jsif it comes up. -
Accepted parsing edges (all low-likelihood, author-trusted input). The preprocessor (
render-core/src/admonitions.rs) is deliberately simple; these diverge from MkDocs only on unusual input and are left as-is:- Body capture is indentation-based and fence-unaware: a fenced code block in
an admonition whose closing fence is de-indented below 4 spaces breaks
capture (the stray
```is then read as a new top-level fence). - An opener is recognised on the line after non-blank text with no blank line
between — more permissive than python-markdown, which requires a preceding
blank. Authors blank-line before
!!!, so it rarely bites. - An
# h1placed inside an admonition body can become the page-title candidate (find_first_h1walks the whole tree). h1-in-callout is unusual. split_titlegarbles a title only when a"sits in the class portion of the opener (bizarre input);deindent4treats one leading tab as 4 spaces.
- Body capture is indentation-based and fence-unaware: a fenced code block in
an admonition whose closing fence is de-indented below 4 spaces breaks
capture (the stray
-
The
color-mix()background tint has no fallback for Safari <16.2 / older engines — the border and title still render (both plainvar()), only the subtle tint is lost. Graceful, not a break.
- Nav collapse is stateless — manual expand/collapse toggles are not persisted.
Each page load recomputes which section(s) contain the current page and renders
only those
open(section_contains_urlincompositor/src/render_page.rs); a section opened by hand re-closes on the next navigation. Deliberate — matches classic MkDocs and needs no JS orlocalStorage. Revisit only if the stateless feel proves annoying in daily use; then persist manual overrides inlocalStorage, merged with the active path.