inker 0.1.0

Modular engine/renderer controller — selects and orchestrates content engines (serval HTML lanes, nematic smolweb, scrying/graft/weld surface engines).
Documentation

inker

inker is the engine/renderer controller for the serval engine family and its hosts (merecat's mere, pelt, strophe, isometry). It owns the question "which engine should handle this content," taking into account the URI scheme, the content type, which engines are actually available on the host, and any user preference — and it defines the three engine kinds the answer dispatches to: document engines (request/response, serializable blocks), session engines (retained documents producing paint frames — the serval HTML lanes, smolweb native), and surface engines (external GPU-texture producers: scrying / graft / weld). One registry pattern, one routing vocabulary, one a11y-capability declaration across all three.

Home: mark-ik/serval, at components/inker (adopted 2026-07). The former standalone repository is archived and links here.

Inker is the right home for arbitrating among engines when several are valid for the same input. For full-web pages, both the Servo/wgpu fork (Serval) and a Wry system webview can serve https://; only one of them may be built or installed on a given host, and the user may prefer one over the other for a specific domain or node. Resolving that is inker's job.

In the printing-press metaphor: inker pairs each engine to its protocol, ready to ink the platen.

What's in the crate

  • document — the portable document model engines produce.

    • EngineDocument carrying address, title, content_type, lang (BCP 47), provenance (source_kind / canonical_uri / fetched_at / source_label), trust (Trusted / Tofu / Insecure / Broken / Unknown), diagnostics (UnsupportedConstruct / DegradedRendering / ParseWarning / RawSourceFallback), and blocks.
    • DocumentBlock variants — structural (Heading, Paragraph, CodeBlock, Quote, List, Image, Preformatted, Rule) plus semantic (FeedHeader, FeedEntry, MetadataRow, Badge). Each documents an AccessKit role; uxtree projects them into a real TreeUpdate.
    • InlineSpan (Text / Code / Emphasis / Strong / Link / SoftBreak / LineBreak), inline_text flattening helper, outgoing_links walker (covers structural inline links + semantic-block URL fields).
    • Round-trip rendering in document::render (impl EngineDocument): to_markdown(), to_gemini(), to_knot(). Knot rendering emits semantic blocks as fenced code blocks with their language tag so they round-trip through nematic::KnotEngine's polyglot parser; to_knot() additionally emits a YAML frontmatter block when the document carries a title, any non-default provenance field, or a known trust state.
    • Per-block provenanceBlockProvenance + BlockProvenanceMap sidecar in document::block_provenance for documents whose blocks came from heterogeneous sources (clips, federated feed merges, citation overlays). Sparse: lookup falls back to the document-level provenance when no per-block override is recorded, so single-source documents pay nothing.
  • engine — the engine trait, input/error vocabulary, and registry.

    • Engine trait — engine_id() -> &str and render(&EngineInput) -> Result<EngineDocument, EngineError>. Implementations live in protocol-specific crates (nematic ships 12; serval for full web).
    • EngineInput — already-fetched content with optional content_type. Network / disk I/O is the host's job; engines stay wasm32-portable.
    • EngineRegistry — engine ID → instance dispatch with register, engine, contains, engine_ids, and dispatch (which honors the decision's engine_id and emits a tracing::warn if the engine is unregistered).
    • EngineError — owned error vocabulary (EngineNotFound, Unsupported, InvalidContent, NotFound, Io, Network).
  • routing — the route-decision vocabulary, default policy, and the full priority chain.

    • EngineRouteRequest carries workspace_id, view, node, address, optional content_type (server-claimed MIME), and optional pinned_engine (per-node engine pin — most authoritative signal).
    • EngineRouteDecisionengine_id + SurfaceContract.
    • EngineRoutePolicy — rules vector, fallback rule, plus per_host_overrides: HashMap<String, String> mapping a case-insensitive host to an engine ID.
    • EngineRouteRule — schemes + content_types + engine_id + mode. Build scheme rules with EngineRouteRule::new(...); build content-type rules with EngineRouteRule::content_type(...).
    • Priority chain (in route_filtered): pin → content-type → per-host → scheme → fallback, all gated by an availability filter (typically |id| registry.contains(id)). Every step skips engines the filter rejects, falling through to the next.
    • SurfaceContract / SurfaceContractMode — host-neutral handoff (CompositedTexture, NativeOverlay, EmbeddedHost, Headless).
    • Helpers: address_scheme(), host_from_address().
  • sniff — best-effort content-type sniffing for unlabelled byte streams (file://, gopher item-1, finger replies, drag-and-drop, on-disk knot files). sniff_content_type(bytes) -> Option<&'static str> matches PNG / JPEG / GIF / WebP / SVG signatures, XML / HTML / Atom / RSS roots, knot frontmatter, gemtext link lines, markdown markers, and falls through to text/plain when the head window has no NUL bytes. Reads at most the first 1 KiB.

  • Engine ID constants: ENGINE_SERVAL_WEB, ENGINE_SCRYING_WEB, ENGINE_NEMATIC_FEED, ENGINE_NEMATIC_FILE, ENGINE_NEMATIC_FINGER, ENGINE_NEMATIC_GEMTEXT, ENGINE_NEMATIC_GOPHER, ENGINE_NEMATIC_GUPPY, ENGINE_NEMATIC_KNOT, ENGINE_NEMATIC_MARKDOWN, ENGINE_NEMATIC_MISFIN, ENGINE_NEMATIC_NEX, ENGINE_NEMATIC_SCROLL, ENGINE_NEMATIC_TEXT, ENGINE_NEMATIC_SMOLWEB (umbrella, kept for back-compat — no protocol routes to it any more), ENGINE_GRAPHSHELL_INTERNAL, ENGINE_EXTERNAL_PROTOCOL.

    ENGINE_SCRYING_WEB is opt-in per tile — not in the default policy. Pinning it via EngineRouteRequest::pinned_engine (or a per-host override) routes the tile through that engine. See design_docs/mere_docs/research/2026-05-11_engine_peers_and_scrying_library_brief.md for the design rationale (preferred non-Servo path; embedded-frame vs overlay composition models).

  • Default policy (full set):

    • Scheme rules: http/https → Serval; gemini/spartannematic.gemtext; gophernematic.gopher; fingernematic.finger; scrollnematic.scroll; misfinnematic.misfin; nexnematic.nex; guppynematic.guppy; filenematic.file; internal schemes (about, graphshell, mere) → headless internal; everything else → headless external-protocol fallback.
    • Content-type rules (win over scheme): text/markdown / text/x-markdownnematic.markdown; text/gemininematic.gemtext; text/plainnematic.text; application/rss+xml / application/atom+xml / application/feed+xmlnematic.feed; text/x-knot / application/x-knotnematic.knot.

How it relates to other workspace crates

inker sits between graphshell (which issues route requests) and the engines themselves; verso-core owns the surface identity inker hands back.

       graphshell::app_state
              │ EngineRouteRequest
              ▼
            inker  ──────►  EngineRouteDecision
              │             (engine_id + SurfaceContract)
              │
              ▼
       engine_id selects: serval | scrying | nematic | wry | internal
                                                       │
                                                       ▼
                                                  verso-core
                                              (SurfaceTargetId)
  • graphshell — emits EngineRouteRequest effects via its EngineRouter service trait; consumes the returned EngineRouteDecision.
  • verso-coreSurfaceContract.target is verso_core::SurfaceTargetId, re-exported through inker::routing for convenience.
  • nematic — implements 12 concrete Engines: markdown, gemtext, gopher, feed, text, file, finger, knot, scroll, misfin, nex, guppy. Hosts register them in one call via nematic::engines() and dispatch through the registry.
  • uxtree — projects EngineDocument into an AccessKit TreeUpdate for OS a11y APIs and inspector overlays; every DocumentBlock and InlineSpan variant maps to an AccessKit role.
  • Serval (Servo/wgpu fork) — referenced by engine ID serval.web; lives outside the mere workspace. The future "three-head Hekate" mode (smolweb extract / middlenet / fullweb negotiator for the same HTML input) is Serval's evolution; nematic explicitly does not own an HTML reader-mode engine.
  • Wry (system webview, third-party) — available as an alternative engine; not in the default policy but a custom EngineRouteRule (or per-host override / per-node pin) can target it.

Status

Pre-1.0 but feature-complete for the engine-controller layer:

  • ✓ Engine trait + registry + dispatch
  • ✓ Portable document model with provenance / trust / diagnostics
  • ✓ Round-trip rendering (markdown / gemini / knot)
  • ✓ Engine availability filtering (route_filtered skips unregistered engines)
  • ✓ Content-type / MIME dispatch (server-claimed type wins over scheme)
  • ✓ Per-domain / per-host overrides (user preference at host granularity)
  • ✓ Per-node engine pinning (most authoritative signal)
  • ✓ 12-engine default policy with full nematic coverage
  • ✓ Content-type sniffing for unlabelled byte streams (sniff module)
  • ✓ Per-block provenance sidecar
  • ✓ Knot frontmatter round-trip

Planned expansions left:

  • Pinned-engine surface mode lookup — currently defaults CompositedTexture; should consult the engine's preferred mode for pinned routes targeting headless engines.
  • Serval "head" preference — when Serval becomes the three-head negotiator, route decisions may need to carry a "preferred head" (smolweb extract / middlenet / fullweb) along with the engine_id.

License

MPL-2.0.