Skip to content

Latest commit

 

History

History
217 lines (187 loc) · 13.4 KB

FOLLOWUPS.md

File metadata and controls

217 lines (187 loc) · 13.4 KB
type reference
links
rel to
part-of
CLAUDE.md

Follow-ups / known limitations

Deferred work and accepted limitations, with enough context to pick up later. Not bugs that block use — conscious deferrals.

serve

  • compositor.toml changes take effect only on restart. The serve watcher 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 .gitignore outside 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 .gitignore does; one in a directory between the project dir and a nested docs_dir, or in a repo root above the project dir, does not — its rules land on the next rebuild, or on restart. The Excluder is rebuilt every cycle (see rebuild_into), so this is staleness of the trigger, not of the rules. Revisit by watching the collected .gitignore paths.

  • 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/ (see root_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 .gitignore entry 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 with serve_handle is 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 its live dot 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's incoming_requests() is a single consumer and respond() writes synchronously, so a slow-reading client briefly blocks other tabs' reload polls. Acceptable ceiling for a local/loopback server; revisit only if serve is put in front of many concurrent real readers. Under serve_handle this 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 give serve_handle a new consequence, though: unbounded shutdown latency. ServeHandle::stop() joins the rebuild thread first (which may be mid-rebuild on a large tree), then unblock()s and joins serve_thread — but unblock() only releases a thread parked in recv(); it cannot interrupt an in-flight respond(), which is synchronous. A client that requests a large asset and stops reading holds shutdown()/Drop until 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 calling Drop on 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 at my 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_image in 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_link also does .md->.html), so they're deliberately not collapsed.

Serve-mode inline editing

  • 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-root home/readme (resolve_home tier 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-root index.md served at / directly, are inline-editable.)

  • Admonitions and wikilink-dense blocks are not inline-editable (data-noedit). preprocess_admonitions rewrites !!! 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 in preprocess_admonitions_mapped handles 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. /__edit only writes a url already in the editable map; 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 what editor.js exports under Node — inline and heading serialization. Reconstruction (range-replacement over the live #doc) needs a DOM and stays covered by the Rust /__edit endpoint tests plus an end-to-end headless-browser pass (edit a block on a page carrying an admonition + code + frontmatter + task list, then diff the file to prove only the edited lines changed). Export more helpers into the harness as they become pure.

  • /__edit does 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 request 400, 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.

Repo-root image resolution (RootAssets)

  • RootAssets's Rewrite can emit a raw #/? that came from the filename, not the author's markup. RootAssets returns a decoded filesystem path, so resolve_image's format!("{u}{suffix}") can splice a literal #/? from a filename into the emitted src. A repo-root README writing ![x](foo%23bar.png) for a real file named foo#bar.png emits src="foo#bar.png"; the browser reads #bar.png as a fragment and 404s, even though the file is copied to site/. Comrak re-encodes a space in the common case, so this is narrow: only a literal # or ? inside a filename triggers it. Only Rewrite is affected, and only RootAssets returns Rewrite — DocsAssets returns Keep, 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_gitignored only sees the .gitignore files Excluder::new collected. collect_gitignores gathers 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 walks target/, which Cargo seeds with a .gitignore containing *, and pruning would need the matchers still being built). So is_gitignored — which judges assets outside the docs tree for RootAssets — consults the repo-root .gitignore (the one that hides scratch in practice) but not a .gitignore sitting in an outside-docs directory, e.g. a repo-root images/.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 .gitignore hides. 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.

Wikilinks (M3)

  • No shortest-unique-suffix path matching. Path-qualified [[dir/Name]] matches the exact rel-path stem only. Obsidian's shortest-unique-suffix resolution ([[Name]] matching a/b/Name when 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 #section to the href without checking the target page actually has that heading id — consistent with the existing .md#frag passthrough. A wrong anchor lands on the page top, not an error.

exclude

  • 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 build that's a hard error, same as any other broken link (--lenient renders it as a 404 instead). By design, not a bug: during a migration, either pass --lenient or remove the link.

Admonitions

  • 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 to compositor.js if 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 # h1 placed inside an admonition body can become the page-title candidate (find_first_h1 walks the whole tree). h1-in-callout is unusual.
    • split_title garbles a title only when a " sits in the class portion of the opener (bizarre input); deindent4 treats one leading tab as 4 spaces.
  • The color-mix() background tint has no fallback for Safari <16.2 / older engines — the border and title still render (both plain var()), only the subtle tint is lost. Graceful, not a break.

Nav collapse

  • 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_url in compositor/src/render_page.rs); a section opened by hand re-closes on the next navigation. Deliberate — matches classic MkDocs and needs no JS or localStorage. Revisit only if the stateless feel proves annoying in daily use; then persist manual overrides in localStorage, merged with the active path.