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`]https://github.com/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 provenance**`BlockProvenance` + `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).
  - `EngineRouteDecision``engine_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`/`spartan`    `nematic.gemtext`; `gopher``nematic.gopher`; `finger`    `nematic.finger`; `scroll``nematic.scroll`; `misfin`    `nematic.misfin`; `nex``nematic.nex`; `guppy``nematic.guppy`;
    `file``nematic.file`; internal schemes (`about`, `graphshell`,
    `mere`) → headless internal; everything else → headless
    external-protocol fallback.
  - Content-type rules (win over scheme): `text/markdown` /
    `text/x-markdown``nematic.markdown`; `text/gemini`    `nematic.gemtext`; `text/plain``nematic.text`; `application/rss+xml`
    / `application/atom+xml` / `application/feed+xml``nematic.feed`;
    `text/x-knot` / `application/x-knot``nematic.knot`.

## How it relates to other workspace crates

inker sits between [`graphshell`](https://crates.io/crates/graphshell) (which
issues route requests) and the engines themselves;
[`verso-core`](https://crates.io/crates/verso-core) owns the surface identity
inker hands back.

```text
       graphshell::app_state
              │ EngineRouteRequest
            inker  ──────►  EngineRouteDecision
              │             (engine_id + SurfaceContract)
       engine_id selects: serval | scrying | nematic | wry | internal
                                                  verso-core
                                              (SurfaceTargetId)
```

- [`graphshell`]https://crates.io/crates/graphshell — emits
  `EngineRouteRequest` effects via its `EngineRouter` service trait; consumes
  the returned `EngineRouteDecision`.
- [`verso-core`]https://crates.io/crates/verso-core`SurfaceContract.target`
  is `verso_core::SurfaceTargetId`, re-exported through `inker::routing` for
  convenience.
- [`nematic`]https://crates.io/crates/nematic — implements 12 concrete
  `Engine`s: `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`]https://crates.io/crates/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.