API Reference
KaimonSlate is normally used through the slate app, its UI, and the agent's slate.* tools. The serving layer below is a small public Julia API for embedding the hub in your own scripts — the app is the everyday entry point.
Serving a notebook
KaimonSlate.NotebookServer.serve_notebook Function
serve_notebook(path; host="127.0.0.1", port=8765, quiet=true)Open the notebook at path in a hub and serve it. Blocks until stopped (Ctrl-C shuts the hub and its workers down cleanly). Once the hub is answering HTTP, prints a framed banner with the openable notebook URL (so a launcher like run.jl surfaces a ready, clickable link rather than a bare port). With quiet=true (default) the console stays clean after the banner: the hub's log detail (worker spawns, connects, warnings) goes to a file in the same tmp dir as the worker logs — the banner shows the path; only errors still print.
KaimonSlate.NotebookServer.start_server Function
start_server(path; host="127.0.0.1", port=8765) -> HubStart a hub and open the single notebook at path. Non-blocking; returns the Hub (stop it with stop_hub). The notebook is served at /n/<id> (printed); / is the index. For a blocking launcher use serve_notebook.
KaimonSlate.NotebookServer.stop_server Function
Stop a hub started by start_server (drains SSE, frees the port).
Hub (multiple notebooks)
A Hub is one HTTP server hosting many notebooks.
Missing docstring.
Missing docstring for KaimonSlate.NotebookServer.start_hub. Check Documenter's build log for details.
KaimonSlate.NotebookServer.open_notebook! Function
open_notebook!(hub, path) -> idLoad the notebook at path into the hub (reusing the existing entry if already open) and start its file watcher. Returns the hub id (its /n/<id> route).
KaimonSlate.NotebookServer.close_notebook! Function
Remove a notebook from the hub: drain its SSE connections and drop it.
KaimonSlate.NotebookServer.stop_hub Function
Stop the hub: drain every notebook's SSE connections, then close the server.
Top-level
KaimonSlate.KaimonSlate Module
KaimonSlateA warm-session, reactive Julia notebook served as a live browser UI — packaged as a Kaimon extension.
Cells evaluate in isolated modules; edits and @bind widgets drive pruned reactive recompute; Makie/MIME figures and interactive ECharts render inline; the source round-trips to a plain .jl file so the agent and the browser share one source of truth.
It runs out-of-process on HTTP 2.0, independent of Kaimon core's HTTP version — the two talk over the Gate (ZMQ), never a shared HTTP stack.
Standalone use:
using KaimonSlate
serve_notebook("notebook.jl"; port = 8765) # blocksAs a Kaimon extension, create_tools exposes slate.open / slate.list / slate.close to the agent, and Kaimon manages the subprocess lifecycle.
KaimonSlate.blob_chunk_mb Method
Configured data-channel chunk size in MB; 0.0 = unset (env / 8 MiB default applies).
KaimonSlate.carry_max_s Method
Configured boot-carry per-entry ceiling in seconds; 0.0 = unset (env / 30s default applies).
KaimonSlate.create_tools Method
create_tools(GateTool) -> Vector{GateTool}Tools exposed to the agent under the slate.* namespace. GateTool is passed in so the extension needs no Kaimon dependency — handlers are plain typed functions, reflected into MCP JSON Schema by Kaimon.
KaimonSlate.ext_prompt_choice Method
The user's onboarding answer for extension registration: "yes" | "dismissed" | "" (not asked).
KaimonSlate.memo_cap_gb Method
Configured durable memo-store cap in GB; 0.0 means unset (worker env / adaptive default applies).
KaimonSlate.on_event Method
on_event(channel, data, session_name)Gate event-bus callback (the extension manifest subscribes to the agent: topic prefix). Kaimon's agent service publishes each agent session's {kind,turn,data} events on agent:<id>; we relay them onto the bound notebook's SSE so the chat pane updates live. Other channels are ignored.
KaimonSlate.on_shutdown Method
on_shutdown()Stop every running notebook server before the extension subprocess exits.
KaimonSlate.parallel_default Method
Whether inter-cell parallel execution is on by default for notebooks (persisted; default true).
KaimonSlate.register_extension Method
register_extension(; auto_start=true, enabled=true, force=false, project_path=pkgdir(KaimonSlate)) -> BoolAdd this package to Kaimon's extension registry (extensions.json in Kaimon's config dir — %APPDATA%\Kaimon on Windows, ~/.config/kaimon elsewhere) so Kaimon loads the slate.* tools automatically — no hand-wiring. Idempotent: returns false (nothing written) if Kaimon isn't installed here or the entry already exists, and true when an entry is added. Registration is consented: the slate app prompts on first run (see app.jl), and loads only self-register when spawned AS the extension (see __init__). Call it explicitly to (re)register a specific project_path or to flip auto_start.
KaimonSlate.remote_config Method
remote_config() -> Dict{String,Any}The optional "remote" object in slate.json — per-host overrides for SSH/connect/tunnel/transfer timing (dial deadlines, ssh ConnectTimeout, tunnel keepalive, blob timeouts, …). Edited by hand; installed into the engine at init. See the _ssh_*/_dial_*/… helpers in remote.jl for every key, its KAIMONSLATE_* env equivalent, and its default. Empty ⇒ every timing keeps its built-in default.
KaimonSlate.run_location_default Method
The machine's GLOBAL default run-location for new notebooks ("host[,transport]"; "" = local).
KaimonSlate.set_blob_chunk_mb! Method
set_blob_chunk_mb!(mb) -> Float64Persist the memo data-channel chunk size (MB per round-trip) and apply it live — no worker respawn needed (the transfer runs hub-side). Smaller chunks keep a slow uplink responsive; bigger ones amortize the RTT. 0 clears back to the env / 8 MiB default. Settings panel knob.
KaimonSlate.set_carry_max_s! Method
set_carry_max_s!(s) -> Float64Persist the boot-window memo-carry ceiling (max seconds any single entry may spend transferring before the cost gate skips it — the cell recomputes remotely instead) and apply it live. 0 clears back to the env / 30s default. Settings panel knob.
KaimonSlate.set_ext_prompt_choice! Method
Persist the onboarding answer (see ext_prompt_choice). Returns the stored choice.
KaimonSlate.set_memo_cap_gb! Method
set_memo_cap_gb!(gb; respawn=true) -> Float64Persist the durable memo-store ceiling (GB) and apply it to future worker spawns; respawn=true recycles running workers so it takes effect immediately. 0 clears the override back to the adaptive default (a quarter of free disk, clamped 2–20 GB). Called by the Kaimon TUI panel.
KaimonSlate.set_parallel_default! Method
set_parallel_default!(on::Bool) -> BoolPersist whether new/re-opened notebooks run cells in parallel by default, and apply it live. The per-notebook Settings toggle still overrides for a specific notebook.
KaimonSlate.set_run_location_default! Method
set_run_location_default!(spec) -> StringPersist the global default run-location (where new notebooks run) to slate.json and apply it live. Per-notebook and per-session overrides still win. Returns the stored spec.
KaimonSlate.set_worker_extra_flags! Method
set_worker_extra_flags!(spec; respawn=true) -> StringPersist extra Julia command-line flags appended to every spawned worker (e.g. "--gcthreads=4,1 --heap-size-hint=4G"), apply it to future worker spawns, and — by default — respawn every running notebook's worker so it takes effect immediately. Returns the stored spec. Called by the Kaimon TUI panel via ctx.eval, same pattern as set_worker_threads!.
KaimonSlate.set_worker_threads! Method
set_worker_threads!(spec; respawn=true) -> StringPersist the worker Julia-thread spec (e.g. "4,1" or "auto"), apply it to future worker spawns, and — by default — respawn every running notebook's worker so it takes effect immediately. Returns the stored spec. Called by the Kaimon TUI panel via ctx.eval.
KaimonSlate.set_xfer_confirm_s! Method
Persist the transfer-preview threshold and apply it live. -1 clears to default; 0 disables.
KaimonSlate.worker_extra_flags Method
Current extra Julia flags appended to every spawned worker (e.g. "–gcthreads=4,1"); "" means none.
KaimonSlate.worker_threads Method
Current worker Julia-thread spec ("<compute>,<interactive>"); "" means the adaptive default (min(cores,8),2).
KaimonSlate.xfer_confirm_s Method
Configured transfer-preview threshold (s); -1 = unset (env / 60s default), 0 = previews off.
The sections below document the internal submodules. These are not part of the stable public API — they're listed for contributors and the curious. Each @autodocs block picks up every remaining docstring in its module (the entry points above are not repeated).
Notebook server
The HTTP/WebSocket serving layer, live-notebook state, history, and agent integration.
KaimonSlate.NotebookServer Module
NotebookServerThe live, interactive notebook backend (interactivity layer 1). Holds a notebook bound to a .jl file and serves a browser SPA plus a small JSON API. Editing a cell reconciles → reactively recomputes only stale cells → persists back to the .jl (so the agent and the browser share one source). Runs CLI-side, wrapping the engine (ReportEngine) and per-cell renderer (ReportRender).
KaimonSlate.NotebookServer.CloudflarePagesTarget Type
Deploy to a Cloudflare Pages project via wrangler pages deploy (token = CLOUDFLARE_API_TOKEN).
KaimonSlate.NotebookServer.EchartsDoc Type
One documented ECharts option path: path (e.g. yAxis.type) and a markdown doc (meaning + values/default + the Slate DSL form).
KaimonSlate.NotebookServer.GithubPagesTarget Type
Deploy to a repo's gh-pages-style branch via [publish_site] (gh + Actions Pages deploy).
KaimonSlate.NotebookServer.NetlifyTarget Type
Deploy to a Netlify site via netlify deploy --prod (token = NETLIFY_AUTH_TOKEN).
KaimonSlate.NotebookServer.ParCell Type
Minimal per-cell info the scheduler needs (built from a Cell's deps/reads/writes/flags).
KaimonSlate.NotebookServer.PublishResult Type
The outcome of deploying one document to one target — the raw material for a ledger Event.
KaimonSlate.NotebookServer.PublishTarget Type
A deploy adapter. Implement publish(::T, nb; …)::PublishResult and preflight(::T)::NamedTuple.
KaimonSlate.NotebookServer.SlateApiEntry Type
One documented Slate helper: name, its category, a signature line, and markdown doc.
KaimonSlate.NotebookServer.ZenodoClient Type
HTTP operations for the Zenodo deposition API, as an interface so tests can inject a fake.
KaimonSlate.NotebookServer.ZenodoHttp Type
Real Zenodo client. sandbox=true targets sandbox.zenodo.org for dry runs.
KaimonSlate.NotebookServer.ZenodoTarget Type
Archive a document to Zenodo as a versioned, citable DOI.
KaimonSlate.NotebookServer._inscope_modules Method
The package/module names in scope for nb: its project deps ∪ the packages its cells using, plus the universal Base/Core. Drives module-scoped doc search so the SHARED index only surfaces THIS notebook's packages, not another notebook's. Error-tolerant — a failure yields the universals.
KaimonSlate.NotebookServer._reconstruct_bundle! Method
_reconstruct_bundle!(jl_path) -> (root, envdir, parent, notebook, fresh, install)Reconstruct a standalone .jl's embedded environment and return its coordinates — the project ROOT, the ENV dir the kernel should activate, the PARENT package dir ("" if none), the reconstructed NOTEBOOK path, fresh (false when it was already populated, reused instantly), and install (true when it landed in a durable user-chosen dir rather than the cache — the caller re-points the notebook there so edits persist).
Destination: the content-addressed depot cache (_bundle_cache_dir) by default, OR — when a launcher (run.jl) sets ENV["SLATE_INSTALL_DIR"] — a durable, user-owned directory THERE (a real project: a git checkout for a full-history bundle). Does not instantiate (the caller does, so download/precompile can be streamed). Cache extraction is staged in a sibling .partial dir and swapped in, so a crash mid-extract can't leave a half-populated cache.
KaimonSlate.NotebookServer._zenodo_deposit Method
_zenodo_deposit(client, depositionId, file, metadata) -> PublishResultThe four-step deposition flow over an already-written bundle file and a ready metadata block: create (or new-version from depositionId) → upload to the bucket → set metadata → publish. Returns a PublishResult carrying the minted doi and the new depositionId in meta. Notebook-free, so the orchestration is unit-testable with a fake ZenodoClient.
KaimonSlate.NotebookServer.agent_add_cell! Method
Add a cell (default code) after after (end if empty) WITH source, run it, return id + result. One file write (build the cell with its source up front) so the async file-watcher can't race the intermediate empty-cell state.
KaimonSlate.NotebookServer.agent_delete_cell! Method
Delete a cell.
KaimonSlate.NotebookServer.agent_edit_cell! Method
Replace a cell's source, run it, return its result.
KaimonSlate.NotebookServer.agent_rename_cell! Method
Rename a cell's id (its label). Ids must be unique + #%%-header-safe; returns a status string.
KaimonSlate.NotebookServer.agent_run! Function
Run one cell (or recompute all stale if id empty); return the result(s).
KaimonSlate.NotebookServer.agent_scratch_eval_bg! Method
agent_scratch_eval_bg!(nb, source; ephemeral=false, grace=_scratch_grace(), memo_*…)
-> (; done::Bool, jobid::String, text::String)Non-blocking scratch eval. Runs agent_scratch_eval! on a background task and races it against grace seconds: finishes in time → (done=true, text=<result>); still running → (done=false, jobid=<id>, text=<hint>), the eval continuing on the worker (poll slate.check_eval). The tool call thus never blocks past grace, so it can't hit the session-tool timeout.
KaimonSlate.NotebookServer.agent_surface_controls! Method
agent_surface_controls!(nb, id, controls; caller="")Surface @bind controls onto cell id's control strip — the agent-facing form of drag-to-host. Presentation only (rewrites the .jl, no re-eval). controls uses the header layout grammar: a,b,c = a row of single controls; [a,b],c = a stacked column [a,b] then a column c; "" clears the strip. Names must be @bind variables defined somewhere in the notebook (validated, so a typo is rejected with the available names rather than silently dropped).
KaimonSlate.NotebookServer.archive_target_names Method
Names of the configured targets that are ARCHIVES (see _ARCHIVE_KINDS), from the live ledger.
KaimonSlate.NotebookServer.build_site! Method
build_site!(dir, nb; site_url="", slug="", bundle=false, kwargs...) -> NamedTupleAssemble/merge nb into the static site at dir (created if absent) WITHOUT any git — build the doc's <slug>/ (or the home front page), upsert slate-site.json, and (re)write the client-side index. The deploy-only building block: render locally, commit dir, let CI just push it. Point several notebooks at the SAME dir to accrete a multi-doc site. site_url is the eventual base URL (for absolute OG tags / bundle fetch); "" ⇒ page-relative. Returns (; home, slug, docUrl, ...).
KaimonSlate.NotebookServer.cell_image Method
cell_image(nb, cell) -> Vector{UInt8} | nothingA PNG of the cell's rendered figure, regardless of where it was drawn: the server-side raster (CairoMakie image/png) if present, else the latest client-captured snapshot (ECharts). nothing if the cell has no viewable figure.
KaimonSlate.NotebookServer.cell_image_fresh Method
cell_image_fresh(nb, cell) -> Vector{UInt8} | nothingLike cell_image, but first refreshes the client-rendered raster from the open browser tab so slate.view is never stale. ECharts cells push a fresh PNG on every render and CairoMakie cells carry an authoritative server-side raster, but markdown / tables / plain-value cells only get a raster on demand — so without this, view returns whatever the last inspect happened to capture. We ask the open tab to recapture (the same round-trip inspect uses; for native-figure cells it's a no-op that leaves the higher-fidelity snapshot intact), then read the freshly-stored image. Skips the round-trip for server-side rasters, and falls back to the last snapshot when no tab is open or the capture times out.
KaimonSlate.NotebookServer.cell_inspect Method
cell_inspect(nb, cellid) -> StringEverything about one cell for the agent's build loop: state (kind/state/deps/reads/writes/ duration/flags), source, the canonical result, and the cell's edit history. The live rendered DOM + optional raster come from the open browser via a separate path (see slate.inspect).
KaimonSlate.NotebookServer.clear_scratch! Method
Empty the notebook's scratchpad and tell the browser to clear its panel.
KaimonSlate.NotebookServer.close_notebook! Method
Remove a notebook from the hub: drain its SSE connections and drop it.
KaimonSlate.NotebookServer.deploy_dir_to_gh_pages Method
deploy_dir_to_gh_pages(repo, dir; private=false, create=true, wait_deploy=true) -> (; ok, url, commit, error)Force-push an already-built site directory dir to repo's gh-pages in one shot (the whole site — no per-doc clone/merge). The "deploy a prebuilt dir" primitive a SITE uses to push its one canonical build to GitHub, mirroring what the S3/Cloudflare/Netlify upload adapters do with the same dir. Creates the repo + enables Pages if needed. Operates on a copy so the canonical local build stays git-free.
KaimonSlate.NotebookServer.ensure_docs_fts! Method
Mirror the docs collection's text + metadata payloads into Kaimon's FTS index (the plain upsert path doesn't), so lexical name/substring search AND module filters work. Idempotent; best-effort if FTS is unavailable. The auto-index path calls this; the manual index_docs tool too.
KaimonSlate.NotebookServer.expand Method
expand(jl_path; target="") -> StringReinflate a standalone .jl (one carrying a Slate.bundle footer) into a project directory at target (default: <jl>.expanded/). A REPO-ROOTED bundle expands to a real git checkout of the original project (its src/, notebooks/, …) with the LIVE notebook cells in place, wired to origin (branch & PR with matching SHAs). A FLAT bundle writes Project + Manifest, any local/ package source, and the notebook at the root. Returns the target dir.
KaimonSlate.NotebookServer.export_gist Method
export_gist(nb; kwargs...) -> NamedTupleCreate a secret GitHub gist holding this notebook's self-contained HTML export, via the authenticated gh CLI. kwargs are the export_html options (theme/charttheme/override/code/ outputs/width/include_source). The gist carries the <slug>.html page plus a README.md that signposts it as a downloadable HTML page (a raw gist shows source, not the rendered page). Returns (; ok, url, preview, raw, curl, error) — preview renders it via gistpreview.github.io, raw is the file's raw URL, curl a ready-to-share download one-liner. Best-effort and never throws: a missing/unauthenticated gh comes back as ok=false with a human-readable error.
KaimonSlate.NotebookServer.export_markdown Method
export_markdown(nb; include_source=true, outputs="all") -> StringSerialize the notebook to GitHub-flavored Markdown for copy-paste (Discourse / Slack / GitHub / Obsidian / docs). Prose rides verbatim; [@cite] and [@fig:label] render to their in-text form (per the notebook's bibstyle) with a trailing References section; code cells become fenced ```julia blocks; text outputs are fenced; figures / frozen charts embed as<img src="data:image/…;base64,…" alt="Figure N"> ; tables become GFM tables. Data-URI images are self-contained but not every host renders them (GitHub strips them) — for those, upload the standalone.jl (+ a PNG/SVG) alongside.
KaimonSlate.NotebookServer.export_pdf Method
export_pdf(nb; include_source=true, style="article", columns=1,
theme="light", code="normal", body="normal") -> Vector{UInt8}Render the notebook to a publication-quality PDF via Typst and return the bytes. style ∈ ("article", "report") picks a layout preset; columns ∈ (1, 2) lays the body out single- or two-column; theme ∈ ("light", "dark") sets the colour scheme (dark matches the live UI and Makie-dark figures). code ∈ ("normal","small","smaller","tiny") sets the code-listing font size, or "hidden" to omit source entirely (also honoured via include_source). body ∈ ("large","normal","compact","small") sets the prose font size (defaults to "compact" for two-column). Figures use vector data when available (CairoMakie PDF, ECharts SVG). @bind controls are omitted by default (a PDF is a static snapshot); set include_params=true to show them frozen to their current values as a parameter strip.
Document metadata is authored as role-tagged cells (#%% md id=… title / abstract / bibliography): the title/abstract are hoisted into an academic title block. With no title cell the document title falls back to the first markdown H1 (then the notebook filename).
KaimonSlate.NotebookServer.export_site Method
export_site(nb; kwargs...) -> Vector{UInt8}Build a self-contained, publishable website for the notebook — index.html (the HTML export, wired to an og-image.png sidecar so a shared link unfurls with a preview) plus that image — as a gzip-compressed tarball. Unpack it into a gh-pages branch / any static host. HTML options (theme, code, outputs, include_source) pass through to the page.
KaimonSlate.NotebookServer.export_to_site Method
export_to_site(nb, name; slug="", bundle=false, base_url="", kwargs...) -> NamedTupleExport nb into the persistent LOCAL site name (created if new), served by the hub at /sites/<name>/. A home notebook becomes the front page; any other notebook gets its <slug>/. The local mirror of [publish_site] — same build_site!, no git/GitHub. Returns (; url, site, slug, home, docCount, dir) where url is the hub-relative path to open. base_url (the site's hub URL) is baked into absolute OG/bundle links when given; "" ⇒ page-relative (fine for local).
KaimonSlate.NotebookServer.export_typst_bundle Method
export_typst_bundle(nb; <same options as export_pdf>) -> Vector{UInt8}The complete Typst PROJECT — doc.typ plus every figure / markdown / code-listing asset it references — as a gzip-compressed tarball (.tar.gz). Unpack it and typst compile doc.typ reproduces the PDF, so the layout and preamble can be tweaked by hand.
KaimonSlate.NotebookServer.figure_index Method
figure_index(report) -> (; numbers, labels, capfor)Resolve figure numbering from caption-tagged cells (document order):
numbers:: caption-cell-id → Figure number (Int)labels:: cross-ref label → (num, anchor) (label =label=attr, else the caption cell id; anchor = the bound figure cell's id when known, else the caption cell id)capfor:: caption-cell-id → bound figure cell id ("" if none resolved)
KaimonSlate.NotebookServer.index_docs! Method
Embed + upsert harvested doc records into the search index. Returns the count indexed.
KaimonSlate.NotebookServer.note_external_tool! Method
note_external_tool!(nb, agent_id, toolname, args, result; ok=true)Gate on caller identity, then surface the write if it came from outside this notebook's crew. agent_id is KaimonGate.current_agent_id() for the call: "" for an outside MCP client (always external), or the owning Kaimon agent's id — external only when it is NOT one of THIS notebook's crew agents, whose calls the agent:<id> relay already streams into the pane (surfacing those here would double them).
KaimonSlate.NotebookServer.notebook_docid Method
notebook_docid(nb) -> (; docId, sourceRepo, sourcePath)The document's stable ledger identity. It is a one-time id embedded in the notebook (meta["docid"], persisted to the .jl footer), so it never changes when the file moves or the repo gains/loses an origin — the failure that used to split one notebook into two ledger entries. Generated + persisted on first use. sourceRepo/sourcePath are derived from git purely for DISPLAY.
KaimonSlate.NotebookServer.og_image Method
og_image(nb) -> Vector{UInt8} | nothingThe social-preview image for the notebook. In priority order: the figure of a cell explicitly tagged thumbnail (or og); else the first real figure in the notebook (a CairoMakie raster, chart, or animation — never a snapshot of text/value output); else a generated title card; else nothing. Used as the og:image for a published page.
KaimonSlate.NotebookServer.open_notebook! Method
open_notebook!(hub, path) -> idLoad the notebook at path into the hub (reusing the existing entry if already open) and start its file watcher. Returns the hub id (its /n/<id> route).
KaimonSlate.NotebookServer.publish_doc_set_targets! Method
Assign/replace the set of target names on this notebook's document (persisted).
KaimonSlate.NotebookServer.publish_document! Method
publish_document!(nb, ledger, docId, store; target_names=nothing, on_event=nothing, kwargs...)
-> Vector{PublishResult}The top-level publish action: deploy docId to each of its ledger targets (or the given target_names) concurrently, append one Event per target to ledger, persist through store (load-merge-save), and return the per-target results. on_event streams per-target progress.
KaimonSlate.NotebookServer.publish_ledger_view Method
The whole ledger as the manager's view model (loads via the default store — may hit the network for gist).
KaimonSlate.NotebookServer.publish_preflight Method
publish_preflight(repo) -> NamedTupleInspect (read-only, no mutation) what publishing to repo would do, so the UI can warn before acting: whether gh is available, the repo exists, its visibility, and whether it already has a gh-pages branch / live Pages site that a publish would overwrite.
KaimonSlate.NotebookServer.publish_secret_set! Method
Set (empty value ⇒ delete) a secret by ref; returns the sorted list of ref NAMES (never values).
KaimonSlate.NotebookServer.publish_set_home! Method
Set/clear whether this notebook is site's front page. Front page is driven by the home tag (model A): this toggles the tag and rebuilds the notebook into the site so its home reflects the change. Setting it as home for one site clears it as home elsewhere it's built (the tag is notebook-global for now).
KaimonSlate.NotebookServer.publish_set_membership! Method
Associate (build into) or disassociate (remove from) this notebook and a site's canonical local build. Local only — a subsequent Publish/Sync deploys. Returns the refreshed publish_sites_info.
KaimonSlate.NotebookServer.publish_site Method
publish_site(nb, repo; private=false, create=true, kwargs...) -> (; url, repo, created, pagesEnabled, pagesError)Publish the notebook as a GitHub Pages site to repo ("owner/name"), using the user's installed + authenticated gh CLI. If the repo is missing and create is set, it's created with the requested visibility (private); the built site is force-pushed to gh-pages, Pages is enabled, and the URL returned. An EXISTING repo's visibility is left untouched. Idempotent — re-runs just update the branch. Requires gh on PATH and gh auth login. (Pages needs a PUBLIC repo on the free plan.)
KaimonSlate.NotebookServer.publish_site_delete! Method
Delete a site. Local removal always: the ledger definition AND the local canonical build dir go away. purge=true additionally tears down deployed content on each of the site's targets where feasible (see purge_deployed!). The view gains a purgeLog entry when a purge ran.
KaimonSlate.NotebookServer.publish_site_set! Function
Create/update a site: its destination targets, home doc, display title ("" ⇒ the site name), and optional per-target subpaths (target → path within that target; "" ⇒ its root). Refuses a (target, subpath) location already claimed by another site — they'd overwrite each other. Membership/order/sections live in its local build.
KaimonSlate.NotebookServer.publish_sites_info Method
For THIS notebook: every site with {member, isHome, targets, url}, plus the known targets — the read model the notebook's Publish panel paints. Membership is matched by the site build's recorded source path (slug as a fallback for pre-migration builds); front page is the site's built home (site_frontpage).
KaimonSlate.NotebookServer.publish_target_delete! Method
Delete a target definition. Removal is LOCAL by default: the ledger entry goes away and every reference (documents AND sites) is detached — deployed content stays live. purge=true also tears down the deployed side where feasible (see purge_deployed!; rsync-serve stops its remote server and removes the served dir). The view gains a purgeLog entry when a purge ran.
KaimonSlate.NotebookServer.publish_to_site! Method
publish_to_site!(nb, siteName; on_event=nothing, deploy=true, slug="", kwargs...) -> summaryBuild nb into the site's canonical local copy (staging it as a member — stamping its source + id), and when deploy=true also Sync the whole build to every destination. deploy=false STAGES only (the local copy is built, nothing is deployed) — the "add a notebook" path under the Stage→Sync model. kwargs are the site-build options (bundle/history/…).
KaimonSlate.NotebookServer.publish_to_targets Method
publish_to_targets(nb, targets; on_event=nothing, kwargs...) -> Vector{PublishResult}Deploy nb to every PublishTarget concurrently, preserving input order in the result vector. on_event, if given, is called on_event(i, :start, target) then on_event(i, :done, result) for each target — the seam the manager UI streams over SSE. Targets are isolated: a throwing/failing target yields an ok=false result and never aborts its siblings.
KaimonSlate.NotebookServer.purge_deployed! Method
purge_deployed!(t::PublishTarget) -> (; ok, log)Tear down what a target DEPLOYED, where that's feasible — the opt-in purge of the removal flows. Only self-hosted kinds can genuinely undeploy; for static hosts (GitHub Pages / Cloudflare / Netlify / buckets) this is a documented no-op — remove the deployed content from the host's own console (or push an empty site) instead.
KaimonSlate.NotebookServer.record_publish_site! Method
record_publish_site!(nb, repo, result) -> StringReflect a successful publish_site to repo in the ledger: ensure a github-pages target for the repo (auto-named, reusing an existing one), ensure this notebook's document (assigned to that target), and append a publish event (live URL, commit SHA, deploy status). Returns the target name. Best-effort — a ledger failure is logged and never fails the publish itself.
KaimonSlate.NotebookServer.relay_agent_event Method
relay_agent_event(channel, data)Gate-bus callback for an agent:<id> event: forward the raw {kind,turn,data} JSON onto the bound notebook's SSE, prefixed agent: so the SPA's live-event handler routes it to the chat pane. data already rides the bus as a JSON string.
KaimonSlate.NotebookServer.reorder_published_site Method
reorder_published_site(repo, ordering) -> (; ok, changed, url, commit)Apply a new section/order to repo's published-site docs and re-push ONLY the manifest + regenerated index to gh-pages (no doc rebuild). ordering is an iterable of dicts {slug, section, order}.
KaimonSlate.NotebookServer.reorder_site! Method
reorder_site!(name, ordering) -> (; ok, docCount)Apply a new section/order to the LOCAL site name's docs and regenerate its index — no git, no deploy (Sync pushes the result to the destinations). ordering is an iterable of {slug, section, order}.
KaimonSlate.NotebookServer.run_publish Method
run_publish(nb, target_names; archive=false, on_event=nothing, build opts…) -> summary::DictLoad the ledger, ensure this notebook's document + its targets, resolve secrets from the config home, fan out the publish concurrently (forwarding the site-build options to each adapter), record one event per target, and persist. on_event(i, phase, payload) streams progress. Throws if a named target isn't configured.
Publishing and archiving are DIFFERENT VERBS on the same store: with archive=false (default) every named target must be a re-pushable live destination — an archive kind (Zenodo) is refused, because a deposit mints a permanent immutable version and must never ride along with a site push. With archive=true the run is a deliberate archival: every named target must be an archive kind.
KaimonSlate.NotebookServer.scratch_check Method
Poll a background scratch job: its result if finished (and forget it), else a still-running note.
KaimonSlate.NotebookServer.search_docs Method
Hybrid docs search over the slate_docs index. ONE search_code call now does the query embed, the semantic+lexical fusion, and span-dedup — replacing the old _embed + _semantic_docs + _fts_docs + hand-rolled fusion (per Kaimon's SEARCH_INTEGRATION_NOTES two-tool model). modules (when non-empty) scopes to those packages via a metadata.module any-of filters on BOTH engines — pass the notebook's in-scope set (_inscope_modules) so a query can't surface another notebook's packages from the shared index. Notes: collection is required (the service endpoint has no workspace binding); embedding_model must match index_docs! (qwen3-embedding:0.6b) or the semantic arm degrades to lexical-only — which still returns name/substring hits.
KaimonSlate.NotebookServer.secret_refs Method
The configured secret ref NAMES (values never leave the process).
KaimonSlate.NotebookServer.serve_notebook Method
serve_notebook(path; host="127.0.0.1", port=8765, quiet=true)Open the notebook at path in a hub and serve it. Blocks until stopped (Ctrl-C shuts the hub and its workers down cleanly). Once the hub is answering HTTP, prints a framed banner with the openable notebook URL (so a launcher like run.jl surfaces a ready, clickable link rather than a bare port). With quiet=true (default) the console stays clean after the banner: the hub's log detail (worker spawns, connects, warnings) goes to a file in the same tmp dir as the worker logs — the banner shows the path; only errors still print.
KaimonSlate.NotebookServer.stage_site! Method
stage_site!(name; hub=nothing) -> {ok,url,buildDir}STAGE the site: (re)build the local copy that is EXACTLY what Sync will deploy. Re-exports every member whose notebook is open (fresh from its live state) and keeps the existing build for the rest (a closed notebook can't be rebuilt without its kernel). Deploys NOWHERE — the staged copy is local + unexposed, viewable at /sites/<slug>/. Sync later just COPIES this artifact to the destinations. Leaves the site "unsynced" (the sync stamp is untouched), since nothing was deployed.
KaimonSlate.NotebookServer.start_server Method
start_server(path; host="127.0.0.1", port=8765) -> HubStart a hub and open the single notebook at path. Non-blocking; returns the Hub (stop it with stop_hub). The notebook is served at /n/<id> (printed); / is the index. For a blocking launcher use serve_notebook.
KaimonSlate.NotebookServer.stop_hub Method
Stop the hub: drain every notebook's SSE connections, then close the server.
KaimonSlate.NotebookServer.stop_server Method
Stop a hub started by start_server (drains SSE, frees the port).
KaimonSlate.NotebookServer.sync_site! Method
sync_site!(name; on_event=nothing, hub=nothing) -> summaryDeploy a site's ONE canonical local build (_site_dir(name)) to every one of its destination targets, concurrently and identically. on_event(i, phase, payload) streams per-target progress. Throws if the site has no local build yet or no configured destinations. Zenodo/non-host targets report an error row.
KaimonSlate.NotebookServer.target_from_ledger Method
target_from_ledger(t::PublishLedger.Target; secrets=Dict()) -> PublishTargetConstruct a runtime adapter from a ledger target config. secrets (a Dict or a ref -> value callable) resolves the target's secretRef for backends that need a token (e.g. Zenodo). CLI-based backends (github-pages via gh, s3/rsync) read their creds from the environment and don't carry a secret on the adapter.
KaimonSlate.NotebookServer.target_name Method
The target's name (its ledger key, e.g. gh:portfolio) — used to attribute the event.
KaimonSlate.NotebookServer.zenodo_request Method
zenodo_request(client, method, url; json=nothing, file=nothing) -> (status::Int, body)The single HTTP primitive the deposition flow is built on. url is either a path under the client's base or an absolute URL (bucket uploads use the deposition's absolute bucket link). json sends a JSON body; file streams a file's bytes (for bucket uploads). Returns the status and parsed-JSON body (or {} on a non-JSON/empty body); never raises on an HTTP error status.
KaimonSlate.NotebookServer.SlateHistory.record! Method
record!(path, source; source_label="browser", kind="checkpoint", cells=nothing, label="") -> entry | nothingRecord a notebook snapshot. Deduped by content hash — returns nothing (and writes nothing) when source equals the latest recorded state. Otherwise appends a delta log entry, stores the (zstd) content object, and refreshes the head keyframe. cells is an iterable of (id, kind, source).
Report engine
The reactive evaluation core — parsing .jl notebooks, the dependency graph, kernels, cells, binds, and paged tables.
KaimonSlate.ReportEngine Module
ReportEngineSession-side engine for the notebook-like report builder (see PLAN-report-builder.md). This is the engine half of the engine/renderer split (§15.1): it owns the cell/document model, parsing the Literate-style .jl source, and (later) isolated-module evaluation + dependency inference. It runs inside the warm gate session and depends only on light, session-safe packages.
This first slice implements just the model + parse/serialize round-trip — no evaluation yet — so it is testable with Base alone.
KaimonSlate.ReportEngine.Animation Type
A precomputed animation: a quantized frame stack + colormap LUT + a small display manifest.
KaimonSlate.ReportEngine.BindSpec Type
A reactive input widget bound to a variable (@bind name Slider(0:100), §Layer 3).
KaimonSlate.ReportEngine.Cell Type
A single report cell. id is the persistent identity (survives edits/moves); src_hash answers "did the source change". Inference/eval fields are populated later by the dependency + eval passes.
KaimonSlate.ReportEngine.Cell Method
Construct a fresh cell, hashing its source and marking it stale (never-run).
KaimonSlate.ReportEngine.CellOutput Type
Captured result of evaluating a code cell.
KaimonSlate.ReportEngine.GateKernel Type
GateKernel(project; parent="", envdir="") <: KernelEvaluate cells in a SlateWorker subprocess pinned to the single environment project.
Environment model (fork-and-extend, never LOAD_PATH-stacked):
Base mode (
project == parent): the notebook has no packages of its own, so it runs directly in the enclosingparentproject — zero overhead, exactly like a plain script.Forked mode (
project == envdir): once the notebook adds a package, it gets its OWN env (envdir) seeded from the parent (parent package dev'd in, parent deps + Manifest copied) and resolved as ONE consistent environment — so the notebook can override the base and there are never two versions of a shared dep. Adds never touch the parent.Detached (
parent == ""): no enclosing project; the notebook env is everything.
envdir is the fork target (the per-notebook env dir); parent is recorded for provenance and re-seeding. Lazily spawns + connects on first use (prepare!).
KaimonSlate.ReportEngine.InMemoryPagedProvider Type
Pages/sorts/filters a column-major dataset in-process (the paged form of slate_table).
KaimonSlate.ReportEngine.InProcessKernel Type
InProcessKernel <: KernelEvaluate cells in the report's own in-process Module. Stateless — the namespace lives on report.mod, managed by report_module / reset_module!.
KaimonSlate.ReportEngine.LocalTarget Type
Run the worker on this machine (the default — unchanged behaviour).
KaimonSlate.ReportEngine.MimeChunk Type
One representation of a cell's output (MIME-generic display bundle, §7).
KaimonSlate.ReportEngine.PageRequest Type
A request for one page: 1-based page, sort_col (0 = none), direction, global search.
KaimonSlate.ReportEngine.PageResult Type
One page of rows plus the total row count of the (filtered) result.
KaimonSlate.ReportEngine.PagedProvider Type
A data source that serves pages on demand. Implement page_columns + fetch_page.
KaimonSlate.ReportEngine.PendingKernel Type
PendingKernel <: KernelPlaceholder installed on LiveNotebook.kernel while the real kernel is booting (a worker spawn) or being reconstructed (a standalone bundle's environment). Every dispatched call BLOCKS until _resolve!/_reject! fires, then forwards to the real kernel — so a run/edit request that races the boot window queues transparently instead of silently evaluating in-process against the wrong (extension's own) environment, or erroring on a worker that doesn't exist yet. Mirrors GateKernel's own lock-guarded prepare! (which blocks concurrent callers on a reconnect), generalized to "any kernel op, before the real kernel is known."
KaimonSlate.ReportEngine.RemoteTarget Type
RemoteTarget(ssh_host; transport=:tunnel, project="~/.cache/kaimonslate/remote")Run the worker on ssh_host — an SSH target you have ALREADY set up (a Host in ~/.ssh/config, key-based auth). We piggyback on that: no password prompts, no host-key negotiation here. transport is :tunnel (firewall-safe, SSH-encrypted) or :direct (CURVE-encrypted straight dial). project is the remote path the notebook's parent project is provisioned into (kept in sync from local).
KaimonSlate.ReportEngine.SlatePagedTable Type
A captured paged table: the registered provider id, columns, total, and page 1.
KaimonSlate.ReportEngine.SlateTable Type
A captured tabular result: typed columns + JSON-safe row cells, rendered client-side.
KaimonSlate.ReportEngine.SqlPagedProvider Type
Browses a SQL relation with sort/filter/paging pushed into the query (slate_query).
KaimonSlate.ReportEngine.Tunnel Type
Tunnel — a supervised `ssh -L` forward set. Respawns the SSH process if it drops
(autossh-lite), so the ZMQ client's reconnect survives a network blip.KaimonSlate.ReportEngine._active_project_deps Method
The in-process kernel's own active project's direct deps as {name, version, uuid} — the same project pkg_op mutates. Mirrors the gate worker's __slate_project_deps.
KaimonSlate.ReportEngine._auto_id Method
Deterministic short id from a cell's content + position (used when none given).
KaimonSlate.ReportEngine._cell_source Method
Emit one cell as a header line plus its body.
KaimonSlate.ReportEngine._expand_cell_statements Method
Expand src's top-level statements in mod (recursively, NEVER evaluating) → the expanded exprs. A macro may return Expr(:toplevel, …) (@enum does) whose sub-statements still carry unresolved hygienic-scope nodes — re-expanding each one resolves them.
KaimonSlate.ReportEngine._expanded_bindings_of Method
ExpressionExplorer analysis of expanded statements → (reads, writes)::Tuple{Set{Symbol},Set{Symbol}}, or nothing (nothing expanded / analysis threw → the caller keeps its conservative scan). ee is the ExpressionExplorer module — each side passes its own import, same pinned version. Hygiene: gensyms ('#' anywhere) and EE's synthetic anonymous-fn names are never notebook bindings and are dropped; a qualified ref on the raw AST surfaces as its ROOT symbol (:Base) — a harmless extra read (no cell ever writes Base), so it passes through.
KaimonSlate.ReportEngine._free_local_port Method
An OS-assigned free local TCP port (bind :0, read it, release — small race window).
KaimonSlate.ReportEngine._graphics_export_names Method
Union of exports of every RESOLVED Makie-family module (empty until one resolves). Compute once per pass and reuse — don't call per cell.
KaimonSlate.ReportEngine._is_graphics_cell Method
True when c touches Makie's shared global state: lexical match, or provenance — any of its reads/provides is an export of a resolved Makie-family module.
KaimonSlate.ReportEngine._is_md_line Method
True for a Literate-style markdown line: # alone or # … (hash + space).
KaimonSlate.ReportEngine._parse_header Method
Parse a header line's trailing tokens into (kind, id, controls, tags::Vector{Symbol}). Every token that isn't id=/controls=/code/md becomes a tag flag (known ones drive behaviour; the rest are free-form metadata that round-trips).
KaimonSlate.ReportEngine._reject! Method
Unblock every waiter on k with a boot failure — forwarded calls raise err instead of hanging forever.
KaimonSlate.ReportEngine._replicate_env! Method
_replicate_env!(t::RemoteTarget) -> nothingReproduce t.origin_env (the notebook's local project) on the remote at t.project: rsync it wholesale (Project.toml + Manifest.toml + any /src), rsync each dev'd dep's source into devsrc/<name> and rewrite BOTH the Manifest path and Project.toml's [sources] path (Julia ≥1.11 resolves dev deps from the latter) to point there, then instantiate. The Manifest makes registry versions exact and clones git deps from their recorded urls; the dev-source rsync makes local checkouts resolve on the host.
KaimonSlate.ReportEngine._resolve! Method
Unblock every waiter on k — subsequent (and in-flight) calls forward to real.
KaimonSlate.ReportEngine._save_asset Method
save_asset(name, data; mime="", dtype=nothing) -> AssetRefRegister data as a named front-end asset produced by this cell — the write-side dual of @asset. Returns an AssetRef that interpolates to a stable, page-local path; load it client-side with Slate.asset(ref). Handles Julia values by type:
a numeric array/matrix → packed as raw column-major binary +
{dtype, shape, order}, so a client reads it straight into aFloat32Array(Slate.assetyields{data, dtype, shape, order}).dtype=downcasts (e.g.dtype=Float32);a
String→ raw text, aVector{UInt8}→ raw bytes (givemime=);any other value (Dict, NamedTuple, …) → JSON (encoded server-side; must be JSON-able).
The bytes ride with the cell's memo, are served live, and are inlined (standalone) or published as a sibling (site) — so a widget/chart works live, offline, and hosted alike.
KaimonSlate.ReportEngine._slate_json Method
Minimal stdlib-only JSON encoding of a value, safe to embed inside a <script>.
KaimonSlate.ReportEngine._strip_blank_edges Method
Trim a leading and trailing run of blank lines, preserving interior blanks.
KaimonSlate.ReportEngine._strip_md Method
Strip the # / # prefix from a Literate markdown line.
KaimonSlate.ReportEngine.animate Method
animate(frames; kind=:heatmap, fps=30, colormap=:auto, clim=:global, transform=nothing,
dither=true, bits=8, x=nothing, y=nothing, title="", colorbar=true,
loop=true, autoplay=false, overlay=nothing, maxbytes=128_000_000) -> AnimationPrecompute a stack of frames ONCE, then play it back entirely in the browser on a WebGL canvas — nothing touches Julia during playback, so a slow simulation still plays at 60 fps. The heavy compute is yours and runs once; animate only quantizes + packages.
kind=:heatmap (default) takes a vector of 2-D scalar matrices, colormapped via colormap/clim, where clim is :global (comparable frames) | :symmetric (signed fields → diverging map; skips transform) | :perframe | (lo, hi). kind=:image takes real color frames — a vector of H×W color matrices (e.g. Matrix{RGB} from VideoIO.jl/Images.jl) or H×W×3 arrays — played back true color, no colormap.
overlay (either kind) draws frame-synced markers on top: a vector with one entry per frame, each a list of (x, y[, id]) points in frame pixel space; id keeps a point's color/trail stable across frames, e.g. a tracked object's identity. Pair with playhead to react to the current frame.
frames = [density(t) for t in times] # heavy compute, once (cache it)
animate(frames; clim=:symmetric, x=r, y=r, title="ψ(t)", autoplay=true)
vidframes = [read(reader) for _ in 1:n] # Matrix{RGB{N0f8}} from VideoIO.jl
tracks = [[(x1,y1,1), (x2,y2,2)] for _ in 1:n] # per-frame (x,y,id) detections
animate(vidframes; kind=:image, overlay=tracks, fps=25, title="tracked beetles")KaimonSlate.ReportEngine.animate Method
animate(f, nframes; …) — generate frame i with f(i) (sugar for animate([f(i) for i in 1:n])).
KaimonSlate.ReportEngine.assign_bind! Method
Set a @bind control's value from the browser: coerce it against the widget, update the per-notebook registry (so a later re-run preserves it), and assign the global so readers see it. Returns the coerced value. Routed through the namespace's injected __slate_set_bind so the logic lives in exactly one place (widgets.jl).
KaimonSlate.ReportEngine.attach_gate_kernel Method
attach_gate_kernel(port, stream_port; project=".") -> GateKernelA kernel bound to an ALREADY-RUNNING SlateWorker reachable at 127.0.0.1:port (+ stream_port) — e.g. a worker on another machine forwarded here over an SSH tunnel (ssh -N -L port:localhost:port -L stream:localhost:stream host). prepare! CONNECTS instead of spawning: no local process, no env reconstruction — the worker owns its process + environment. The transport is unchanged (the hub always connects to 127.0.0.1:port), so the tunnel is transparent. Remote execution for a notebook is then: start the worker there, forward the two ports, hand the notebook this kernel.
KaimonSlate.ReportEngine.build_dependencies! Method
build_dependencies!(report) -> reportRe-infer bindings and compute each code cell's upstream deps (ids). A cell depends on the most recent prior writer of each name it reads, plus any earlier cells its needs= tag names (user-asserted effect edges). An :opaque cell depends on all prior code cells, and all later code cells depend on it (barrier).
KaimonSlate.ReportEngine.cancel_cells Method
cancel_cells(kernel, report, ids) -> IntBest-effort interrupt of the named cells' IN-FLIGHT evaluator tasks (superseded-edit preemption — see _preempt_superseded!). Never a correctness dependency: the src-hash version guard still discards a stale result on completion. In-process evals are synchronous with their caller — nothing to preempt — so the base method no-ops; the gate kernel forwards to its worker.
KaimonSlate.ReportEngine.complete Method
complete(kernel, report, code, pos) -> (; items, from, to)Completion candidates for code at byte offset pos, resolved WHERE the kernel's bindings live — so using'd packages and evaluated-cell bindings complete, not just Base. items is a Vector{Tuple{String,String}} of (text, kind); from/to are 0-based byte offsets of the replaced range. The in-process kernel completes locally.
KaimonSlate.ReportEngine.dependents_of Method
dependents_of(report, ids) -> Set{String}Transitive closure: ids plus every cell that (transitively) depends on one of them. This is the staleness blast radius of changing ids.
KaimonSlate.ReportEngine.echart Method
Build an interactive ECharts chart from an option dict.
KaimonSlate.ReportEngine.eval_capture Function
Evaluate source in the kernel and capture stdout + rich output → CellOutput. region/regions seed the task-local Slate execution context (see _build_slate_ctx); region="" ⇒ the main kernel.
KaimonSlate.ReportEngine.eval_report! Method
eval_report!(report; reset=false, kernel=InProcessKernel()) -> ReportEvaluate all code cells in document order through kernel. reset=true does a full rebuild in a fresh namespace first. (No dependency pruning here — that's eval_stale!; this runs every code cell.)
KaimonSlate.ReportEngine.eval_stale! Function
eval_stale!(report, kernel=InProcessKernel()) -> reportEvaluate only STALE code cells, in document order, through kernel. Unchanged (FRESH) cells keep their cached output — their effects already live in the kernel's namespace from the prior eval. (First run: all cells stale ⇒ full eval.)
KaimonSlate.ReportEngine.fetch_page Function
fetch_page(provider, ::PageRequest) -> PageResult — one page, sorted/filtered/paged.
KaimonSlate.ReportEngine.fetch_remote_worker_log Method
fetch_remote_worker_log(k; maxbytes) -> StringTail the SSH host's own worker-<port>.log (the remote Julia process's stdout/stderr: KaimonGate load, serve() banner, eval output, crashes). This is the factorio-side record — fetched over the same authenticated SSH channel we spawned it on, so it's visible locally in the browser worker-log.
KaimonSlate.ReportEngine.gate_available Method
True when running inside the Kaimon extension (gate client available).
KaimonSlate.ReportEngine.harvest_docs Method
harvest_docs(kernel, report, mod_names) -> Vector{Dict}Harvest {module, name, doc} for documented exported bindings of the named modules, resolved WHERE cells evaluate (so the modules must be using'd in the notebook). The gate kernel forwards to its worker, where the notebook's packages live.
KaimonSlate.ReportEngine.harvest_module_docs Method
harvest_module_docs(where, mod_names) -> Vector{Dict}For each module named in mod_names (resolved in module where, so a notebook must have using Foo first), collect {module, name, doc} for every documented exported binding. The docstring already carries the signature, so it isn't extracted apart.
KaimonSlate.ReportEngine.interpolate Method
interpolate(kernel, report, exprs) -> Vector{CellOutput}Capture each markdown in the kernel (rich output, like a mini code cell). The gate kernel forwards to its worker.
KaimonSlate.ReportEngine.latex_symbol Method
latex_symbol(name) -> StringResolve a LaTeX/emoji completion command ("\alpha", "\:smile:") to its character; "" if unknown. A PARTIAL latex query (\alph) comes back from REPLCompletions as the NAME, not the symbol — so the UI displays the name (it must, to filter by what the user typed) but resolves the symbol via this to APPLY in one step (else accepting inserts the literal \alpha).
KaimonSlate.ReportEngine.list_remote_workers Method
list_remote_workers(host) -> Vector{Dict}Enumerate the workers Slate has spawned on host, each: port, alive (process running), lastActivity (unix mtime of its log = last computation), logBytes, state/stateSince (lifecycle sidecar: "attached"/"idle" + unix ts; "" for a pre-sidecar worker — advisory, a hub crash leaves a stale "attached"), manifest (raw JSON string — the browser parses it for who/what/when), and stats (the worker's latest 2s telemetry sample: cpu/rss/gc_ms/evals/memo_bytes/ts as raw JSON; "" for a pre-telemetry worker). Reads the on-host manifests over one ssh call. [] if unreachable/none.
KaimonSlate.ReportEngine.macroexpand_cells Method
macroexpand_cells(kernel, report, srcs) -> Dict{String,Tuple{Set{Symbol},Set{Symbol}}}Macro-expand each cell source (id => source) in the namespace where cells evaluate — recursively, NEVER evaluating — and ANALYZE the expansion there, returning id => (reads, writes). Cells whose expansion/analysis fails are omitted (the caller keeps its conservative static analysis). The gate kernel forwards to its worker, where the notebook's macros are actually defined; only name lists cross the wire (see macroexpand.jl).
KaimonSlate.ReportEngine.memo_pin! Method
memo_pin!(kernel, report, key::AbstractString, pin::Bool) -> nothingPin (pin=true) or release (pin=false) a memo entry against gc eviction — the locked cell tag's durability guarantee (§ set_cell_tags!). The in-process kernel has no durable memo store, so this is a no-op there.
KaimonSlate.ReportEngine.module_help Method
module_help(kernel, report, name) -> DictLive help lookup for name (a binding or module), resolved WHERE cells evaluate — {name, module, doc, kind, exports}. Powers the docs palette's ?Module drill-down (list a package's exports) + cross-reference links. The gate kernel forwards to its worker, where the notebook's packages live.
KaimonSlate.ReportEngine.module_help Method
module_help(where, name) -> DictResolve name in module where (the package must already be using'd / imported there) and return a help record: {name, module, doc, kind, exports}. kind is "module", "function", "type", "const", or "unknown". For a Module, exports lists its exported bindings as {name, kind} (sorted) for drill-down; empty otherwise. doc is the raw @doc text (markdown). Pure (Base.Docs + reflection only) so it loads into the dependency-light worker, exactly like harvest_module_docs.
A bare identifier that doesn't resolve is retried case-insensitively (exact case still wins), so regionplan finds RegionPlan — see _ci_resolve_name.
KaimonSlate.ReportEngine.page_columns Function
page_columns(provider) -> Vector{ColumnDef} — the provider's columns.
KaimonSlate.ReportEngine.parse_report Method
parse_report(text; id="r", title="") -> ReportParse hybrid source into a Report. Two interchangeable conventions are accepted — liberal in, canonical out (serialize_report always emits the explicit #%% form):
Explicit percent cells: a
#%% [code|md] [id=…]header introduces a cell whose body runs verbatim until the next header.Literate-style (implicit): before any header, a run of comment lines becomes a markdown cell only when it reads as prose (a heading/list/rule marker, a blank line before the code, or no code after it); an ordinary comment that hugs the code below it stays an inline comment on that code cell. Bare code runs are code cells. See [
_append_implicit_cells!].
Pure-Literate files, pure-percent files, and Literate-then-percent mixes all parse. (Once a #%% header appears, subsequent cells should also use headers — implicit content after a header is taken as that explicit cell's verbatim body.)
KaimonSlate.ReportEngine.pending_macro_cells Method
Flagged cells whose macro bindings are still unrecovered (no cache entry, not marked tried).
KaimonSlate.ReportEngine.pkg_op Method
pkg_op(kernel, report, op, name) -> Dict{String,Any}Add (op="add") or remove (op="rm") a package in the kernel's active project — the notebook's own dependency environment. The gate kernel mutates its worker's project; the in-process kernel has no separate worker to fork an env in, so cells already run in the process's own active project — same "env IS the whole world" semantics as a GateKernel's detached notebook (server.jl's parent == "" case). target is accepted for API parity with GateKernel but has no separate object to select (no parent to add to instead). Returns {ok, message}.
KaimonSlate.ReportEngine.preflight_remote Method
preflight_remote(host; transport=:tunnel, on_step=nothing) -> DictTest + prime host: ssh reachability, Julia presence (+version), env provisioning, KaimonGate load, CURVE key (for :direct), then a real spawn → connect → round-trip eval → clean teardown. Returns Dict("host","transport","ok",steps=>[{name,status,detail,ms}…]). Idempotent and self-cleaning: it leaves the primed env behind (so the first real run is fast) but no worker or tunnel running. on_step(::PreflightStep), if given, is called as each step STARTS (status "run") and COMPLETES — so the SSE endpoint can stream progress live instead of blocking for the whole (minutes-long) run.
KaimonSlate.ReportEngine.prepare! Method
Ensure the kernel's namespace exists and is ready to evaluate into.
KaimonSlate.ReportEngine.prewarm_macros! Function
prewarm_macros!(report, kernel=InProcessKernel()) -> BoolPre-eval macro expansion (peer of prewarm_usings!): recover unknown-macro bindings BEFORE a run so the graph — and the memo keys derived from it — is precise from the first eval. Package macros (Base.@kwdef, @enum, DataFrames' @chain, …) expand here because prewarm_usings! already imported their modules; notebook-defined macros resolve post-drain in refine_macros!. Failures are NOT marked tried — the macro may get defined during the run. Returns true iff something newly resolved (deps rebuilt).
KaimonSlate.ReportEngine.prewarm_usings! Function
prewarm_usings!(report, kernel=InProcessKernel()) -> BoolPre-eval counterpart of refine_usings!: resolve bare-using exports BEFORE a run, so the dependency graph — and every memo key derived from its upstream closures — is computed in precise form from the very first eval of a session. Without this, a fresh session analysed using X as an :opaque barrier, keyed the first run's memo entries against that conservative graph, then flipped to the precise graph post-drain (refine_usings!) — so every cell below a using changed memo keys between the first and second run of each session and MISSED the durable cache exactly when it mattered most (cold open). Returns true iff a module was newly resolved (deps rebuilt).
KaimonSlate.ReportEngine.project_deps Method
project_deps(kernel, report) -> Vector{Dict}The notebook project's direct dependencies as {name, version} (for eager docs auto-indexing — everything reachable is worth indexing, so this is intentionally unfiltered). The gate kernel reads its worker's active project; the in-process kernel has no separate worker, so it reads the host's own active project — the same one pkg_op adds/removes into.
KaimonSlate.ReportEngine.provision_remote! Method
provision_remote!(t::RemoteTarget, parent_project) -> nothingIdempotent. Ensure the host can run a SlateWorker: (1) rsync Slate's worker payload, (2) materialise a KaimonGate worker env (added from the registry) + Revise, instantiate, (3) rsync the notebook's parent project (Project.toml + /src) and instantiate it. Cheap on reruns (rsync only ships deltas; the env instantiate is skipped once .ready exists).
KaimonSlate.ReportEngine.pull_blob! Method
pull_blob!(host_ip, data_port, hash; server_key="", timeout_ms=20_000) -> IntPULL one content-addressed blob from a worker's store over the data channel ('G' chunks) into the LOCAL memo CAS — the reverse of push_memo_blobs!; how remote results flow back (region runner: a remote cell's writes that local cells read). Streams into a tmp file, sha-verifies against the address, atomic-renames — a truncated/corrupt transfer never lands. Dedup first: an already-present blob costs nothing. Returns bytes moved over the network (0 = deduped).
KaimonSlate.ReportEngine.push_blob! Method
push_blob!(host_ip, data_port, hash; server_key="", timeout_ms=20_000) -> IntShip ONE blob from the LOCAL CAS to a worker's store over its data channel (dedup-aware: the 'H' query makes an already-present blob cost one round-trip). The region runner's local→remote half; pull_blob! is the reverse. Returns bytes actually sent (0 = deduped).
KaimonSlate.ReportEngine.reap_remote_worker Method
reap_remote_worker(host, port) -> BoolExplicitly kill the worker on host:port and remove its script/log/manifest. Manual only — Slate never auto-reaps (a worker may hold results worth keeping). Returns true if the kill+cleanup ran.
KaimonSlate.ReportEngine.refine_macros! Function
refine_macros!(report, kernel=InProcessKernel()) -> BoolPost-drain macro expansion (peer of refine_usings!): by now every macro a cell could define or import has had its chance to exist, so expand the still-pending cells and mark failures tried (attempt-once per source; an edit to the cell — or to a macro-defining cell, see update_source! — clears the way for a retry).
Unlike refine_usings! (which only NARROWS), recovering a macro-hidden WRITE adds an edge. A PARALLEL drain was scheduled without it, so a reader may have raced its producer (errored on the not-yet-defined name, or silently consumed the previous run's value) — with restale_racers = true (the parallel server path) everything downstream of a newly-recovered writer is restaled once ("staleness never under-invalidates") and the caller's runner re-arms. A serial drain executes in document order — a valid topological order even without the edge — so the default skips the restale. Returns true iff something newly resolved (deps rebuilt).
KaimonSlate.ReportEngine.refine_usings! Function
refine_usings!(report, kernel=InProcessKernel()) -> BoolPost-eval progressive precision: for each code cell still an :opaque barrier because of a bare using X that has now SUCCESSFULLY run, resolve X's exports and cache them, then rebuild the dependency graph so the barrier becomes a precise import. No cell is restaled — this runs after a drain, and narrowing a cell's dependents can only shrink the blast radius (see [build_dependencies!]). Idempotent: each module path is attempted at most once per session. Returns true iff a module was newly resolved (and deps were rebuilt).
KaimonSlate.ReportEngine.remote_log Method
Tail the durable remote-orchestration log (last maxbytes).
KaimonSlate.ReportEngine.report_module Method
Get (creating if needed) the report's execution namespace.
KaimonSlate.ReportEngine.reset! Method
Discard the kernel's namespace (full rebuild); cells are marked stale by the caller.
KaimonSlate.ReportEngine.reset_module! Method
reset_module!(report) -> ModuleDiscard the report's namespace and mark every cell stale — the basis of a full rebuild (ground truth, §6). Returns the fresh module.
KaimonSlate.ReportEngine.resolve_macros! Method
resolve_macros!(report, kernel, cells; mark_tried=false) -> BoolRound-trip cells' sources to the kernel for macro expansion (ONE batched call), re-analyze each expanded form, and cache the recovered (reads, writes). Returns true iff anything newly resolved (the caller rebuilds the graph). With mark_tried, a cell whose expansion failed is recorded so it isn't round-tripped again (post-drain semantics — its macros had their chance to be defined); the pre-run pass leaves failures unmarked so the post-drain pass can retry them.
KaimonSlate.ReportEngine.revise_apply! Method
Apply pending parent-/src revisions in the worker (Revise) → the changed top-level def-names.
KaimonSlate.ReportEngine.series Method
series(kind, args...; name=nothing, kwargs...) -> EChartSeriesBuild one series for the composable echart(series(…), series(…); …) form — combine different kinds and axes in a single chart. kind is an ECharts series type:
:line/:bar/:area(x, y)— string x → category axis, numeric x → value axis:scatter(x, y):pie(labels, values):heatmap(z::Matrix)or(xlabels, ylabels, z)— adds category axes + a visualMap:candlestick(dates, ohlc)—ohlc[i] = [open, close, low, high]:radar(indicators, values)—indicators = ["Sales" => 6500, …]; values a vector, or["Allocated" => […], "Actual" => […]]for several rings:boxplot(categories, data)— eachdata[i]is[min,Q1,med,Q3,max]or raw samples:sankey(links)or(nodes, links)— flows; each link(source, target, value)orsrc => tgt => val:graph(edges)or(nodes, edges)— a network; each edge(source, target)orsrc => tgt(force layout):treemap/:sunburst(tree)— a hierarchy:name => value(leaf),name => [children…](branch), or NamedTuple/Dict nodes; pass one root or a vector of roots:lines(from, to)or(flows)— geo trajectories/flows; coords(lon, lat); bindscoordinateSystem="geo"(passgeo=(map="world",…)+registerMap):calendar(dates, values)— a calendar heatmap; brings the calendar component + a visualMap
Any other kind falls back to data = args[1]. name= labels the series for the legend; every extra kwarg (smooth, stack, symbolSize, yAxisIndex, areaStyle, markLine, lineStyle, …) splices into the series option verbatim.
echart(series(:bar, x, a; name="obs", stack="t"),
series(:line, x, b; name="fit", smooth=true, yAxisIndex=1); legend=true,
yAxis=[(name="obs",), (name="fit", type=:log)])KaimonSlate.ReportEngine.set_bind_value! Function
Convenience: set the sole bind of a single-control cell (no-op unless exactly one).
KaimonSlate.ReportEngine.set_bind_value! Function
set_bind_value!(report, cell, name, value, kernel=InProcessKernel()) -> cellApply a browser value change for bound variable name (one of cell.binds): route it through the kernel (assign_bind! coerces against the widget, updates the per-notebook registry, and assigns the global), then mirror the coerced value into the host-side BindSpec. No-op if the cell has no such bind.
KaimonSlate.ReportEngine.shutdown! Method
Shut the kernel down and clear gate state. A LOCAL worker is killed (clean exit request + SIGTERM/SIGKILL backstop). A spawned-remote worker is DETACHED by default — tunnel/sync closed, process left running warm (namespace + packages + memo store) with its state sidecar flipped to idle, so reopening the notebook reattaches instantly. kill_remote=true is for the paths where a surviving worker would be wrong: an explicit restart (reattach would make it a no-op), the preflight probe, and reap. An ATTACHED worker (k.remote) is never ours to kill either way.
KaimonSlate.ReportEngine.shutdown! Method
Release a kernel's resources (kill a local gate worker; detach a spawned-remote one unless kill_remote=true — see the GateKernel method). No-op for in-process.
KaimonSlate.ReportEngine.slate_completions Method
slate_completions(mod, code, pos) -> (; items, from, to)REPLCompletions against mod at byte offset pos. items is a Vector{Tuple{String,String}} of (text, kind); from/to are 0-based byte offsets of the range the completion replaces (CodeMirror-ready). Returns a NamedTuple so it rides the gate wire to the server unchanged.
KaimonSlate.ReportEngine.slate_fingerprint Method
slate_fingerprint(xs...) -> StringA canonical, session-stable content hash (SHA-256 hex) of the given value(s) — isequal-style semantics: Dicts/Sets are order-independent, NaN ≡ NaN, integer widths widen, missing and nothing are distinct. The robust way to assert a restored/recomputed/transferred value is REALLY the same — unlike hash, which leaks Dict order and session state. slate_fingerprint(df, params) in a cell gives one comparable line across runs, sessions, and worker restarts.
KaimonSlate.ReportEngine.slate_matrix Method
slate_matrix(M::AbstractMatrix; kind=:auto, rows=nothing, cols=nothing, max_cells=200*200,
downsample=true, colors=["transparent","#569cd6","#ffd700"],
blockrows=nothing, blockcols=nothing, digits=3)Render M as whichever form suits its size and structure: an exact KaTeX bmatrix (small), symbolic dotted notation (large + a recognized banded type — Diagonal/Tridiagonal/ SymTridiagonal/Bidiagonal/triangular), or a downsampled ECharts heatmap (large + anything else, including sparse). Any bare AbstractMatrix returned from a cell renders this way automatically; call slate_matrix explicitly to override the choice or its defaults.
kind— force:katex/:dotted/:heatmapinstead of auto-picking.rows/cols— crop to a sub-region BEFORE rendering (e.g. one tile of a periodic or block-repeating large matrix), as ranges.max_cells— the heatmap's downsample target (default 200×200 cells).downsample=falseforces full resolution — only safe for a matrix that already fits (pair withrows/colsto crop a large one down first; forcing full resolution on a large matrix directly can be extremely slow/memory-heavy).colors— the heatmap'svisualMapcolor ramp, low → high.blockrows/blockcols— block sizes for divider lines in the KaTeX form (e.g.[2,2]for a 4×4 matrix drawn as four 2×2 blocks).digits— rounding for the KaTeX forms.
KaimonSlate.ReportEngine.slate_memo_entries Method
slate_memo_entries(; name = "") -> Vector{NamedTuple}The durable memo store's entries, newest first — one row per cached cell result: (; key, names, bytes, blobs, created) where names are the globals the entry restores and blobs their content hashes (shared hash across rows = deduped storage). name = "x" filters to entries carrying a binding named x. Return it from a cell (or wrap in slate_table) to see exactly what a cold open will restore.
KaimonSlate.ReportEngine.slate_memo_stats Method
slate_memo_stats() -> (; manifests, blobs, bytes, root)Shape of the durable memo store backing this notebook's cache tags: entry count, unique content blobs (identical values dedup to one), total on-disk bytes, and the store root. In-process kernels have no durable store — all zeros.
KaimonSlate.ReportEngine.slate_query Method
slate_query(conn, sql; page_size=50) -> SlatePagedTableBrowse the result of sql (run against DBInterface connection conn, e.g. a DuckDB.DB or SQLite.DB) as a server-paged table: sorting, global search, and paging are pushed into SQL, so the browser only ever holds one page. The whole result set is never materialized.
KaimonSlate.ReportEngine.slate_table Method
slate_table(data; format, align, coltype, viz, default_format, paged, page_size, export_rows) -> SlateTable
slate_table(columns, rows; format, align, coltype, viz, default_format, export_rows) -> SlateTableBuild an interactive, sortable, filterable, paged table. RETURN it from a cell to render it — a bare DataFrame / Tables.jl source already auto-renders, so you only call slate_table explicitly to pass options. data may be a DataFrame / any Tables.jl source, a Vector of NamedTuple rows, or a Dict/NamedTuple of equal-length column vectors. The two-argument form takes explicit columns (names) plus rows (a vector of row vectors/tuples, or an AbstractMatrix). Cells are reduced to JSON-safe scalars; numbers stay numeric so the browser sorts them numerically.
Each column's physical type (:int/:float/:bool/:date/:string) and default alignment are inferred. The overlay options each take a NamedTuple/Dict keyed by column NAME — a ONE-entry NamedTuple needs its trailing comma: (Revenue = :currency,):
• format — per-column display formatting. A value is a preset Symbol, or a NamedTuple/Dict naming a kind plus overrides (digits, sep, prefix, suffix). Presets: :currency ($, 2 dp, thousands-grouped) · :percent (1 dp) · :integer (grouped) · :fixed (2 dp) · :scientific (3 sig figs) · :bytes (KB/MB/GB…). • align — :left / :right / :center, overriding the type-inferred default. • coltype — override the inferred physical type (e.g. force an id column to :string). • viz — an in-cell visualization for a NUMERIC column, scaled over its min→max: :bar (a proportional bar behind the value) or :heat (a background shaded by magnitude). • default_format — one format spec (same DSL as a format value) applied to EVERY numeric column that doesn't have an explicit entry in format. Handy for a blanket default_format = :integer (round-to-nearest-int) instead of listing every column.
Example combining several:
slate_table(df; format = (Revenue = :currency, Margin = (kind = :percent, digits = 1)),
align = (Product = :left,),
viz = (Revenue = :bar, Margin = :heat))paged = true builds a SERVER-paged table: the provider stays where cells evaluate and the browser fetches one page_size-row page at a time, so the full result set never crosses the wire (use it for large data — slate_query(conn, sql) does the same for a SQL source). export_rows = n caps the rows shown in FIXED exports (PDF / markdown / static HTML) to the first n (with a "showing n of N" note); the live table stays fully paginated.
KaimonSlate.ReportEngine.slate_theme Method
slate_theme(; theme="") -> Makie.ThemeThe shared Slate look as a Makie Theme, built from the same brand palette the interactive ECharts figures use — transparent background, the Slate series colours, palette-toned grid/axes, and a default figure size that matches the ECharts cell height. Needs Makie loaded (using CairoMakie).
theme selects a palette by name ("midnight", "nord", "daylight", …); the default "" follows the ACTIVE UI theme, so a rendered plot matches whatever Slate theme is on. Compose extra styling on top with Makie's own update_theme!/set_theme!(base, overlay).
using CairoMakie
set_theme!(slate_theme()) # follow the active UI theme
set_theme!(slate_theme(theme="nord")) # or pin a specific palette
# or just: use_slate_theme!()KaimonSlate.ReportEngine.source_text Method
Alias kept for callers that think in terms of a cell's raw text.
KaimonSlate.ReportEngine.spawn_and_connect_remote! Method
spawn_and_connect_remote!(k, t::RemoteTarget, parent_project) -> (conn, tunnel|nothing)Provision (idempotent) + start a SlateWorker on the host + connect the hub's kernel to it, CURVE-pinned (direct) or over a supervised SSH tunnel. k is the GateKernel (its .project is the REMOTE project path; .port/.stream_port are set here). Returns the REPLConnection and the Tunnel (or nothing). Also starts the continuous /src sync.
KaimonSlate.ReportEngine.standalone! Function
standalone!(m::Module = @__MODULE__; dir = nothing) -> ModuleInject the notebook-namespace contract into m so a Slate .jl runs as plain Julia (julia notebook.jl / include), Pluto-style. This is the single lever that makes a notebook runnable outside the Slate server: the same _populate_notebook_ns! contract is installed, but the live-only features degrade to no-ops.
@bind x W(…)→x = W's default (empty registry, no browser wiring) — the real bind path, so it works with no special-casing.echart/slate_table/slate_querybuild their display objects as usual (pure constructors); they render viashow/MIME if the run is display-capable, else are inert.slate_refresh/slate_progress/slate_emit/ reactive fires → no-op.@asset/readfile/datadir/@sfileresolve againstdir(the notebook file's directory), defaulting topwd().
Idempotent: a second call — or the Slate engine re-populating the same module — is a no-op (guarded on the __slate_standalone marker), so the emitted preamble never double-injects.
KaimonSlate.ReportEngine.table_page Method
table_page(kernel, report, table_id, request) -> (rows, total)Fetch one page of a server-paged table (a slate_table(…; paged=true) / slate_query result), routing to the provider registered where cells eval. The request is the frontend's JSON body (page / page_size / sort_col / sort_desc / search). In-process providers live here; the gate kernel forwards to its worker.
KaimonSlate.ReportEngine.update_source! Method
update_source!(report, new_source) -> reportReparse new_source, reconcile cells by id (carrying over cached output for cells whose source is unchanged), rebuild the graph, and mark changed cells + their transitive dependents STALE. Removed cells invalidate their former readers. Does not evaluate — call eval_stale! next.
KaimonSlate.ReportEngine.use_slate_theme! Method
use_slate_theme!(; theme="") -> nothingApply the slate_theme globally (Makie.set_theme!) so every Makie figure in the notebook matches the interactive ECharts look. theme names a palette; "" follows the active UI theme. Needs Makie loaded. Call it once in a setup cell (re-run it after switching the UI theme to re-render).
Report rendering
Turning evaluated cells into HTML/markdown output.
KaimonSlate.ReportRender Module
ReportRenderThe renderer half of the engine/renderer split (§15.1): turns an evaluated Report into a self-contained HTML artifact. Runs CLI-side, where the heavier template deps already live (OteraEngine, CommonMark). It only consumes the engine's Report/Cell data — no live module needed.
Escaping is correct by construction: one OteraEngine template with autoescape=true HTML-escapes all code/stdout/value text automatically; CommonMark-rendered markdown (already safe HTML) is injected via the |> safe filter. No hand-escaping (§8).
KaimonSlate.ReportRender.markdown_html Method
Markdown source → HTML (live notebook + md cells). interps are the captured outputs of the cell's blocks, spliced in document order.
KaimonSlate.ReportRender.output_html Method
output_html(cell) -> StringThe output fragment for a code cell (stdout / value / rich display / error), escaped and embedded. Used by the live notebook server to update a cell in place.
KaimonSlate.ReportRender.render_html Method
render_html(report) -> StringRender an (already-evaluated) Report to a self-contained HTML string.
KaimonSlate.ReportRender.render_report_file Method
render_report_file(path; out=..., title="", reset=false) -> out_pathEnd-to-end convenience: read a hybrid .jl, parse → evaluate → render → write HTML. Returns the output path.