How a read client reaches a KB — directly in-process, or over the daemon HTTP API
(vaelii.host.serve). It re-exports the slice of the vaelii.core read surface the
browser uses, so the browser is written once against these names and runs unchanged
against a local KB or a remote daemon.
A target is either a real KB (treated as local) or an access value from local /
remote. A KB-read op dispatches on it:
:remote → the client (vaelii.host.client/call) — one HTTP round-trip
:local → vaelii.core, via serve/ops (the very allowlist the daemon serves, so
local and remote answer through the same table and cannot drift)
a raw KB → the same local path (so a caller holding a plain KB needs no wrapper)
The pure display fns (term-role, reified-term?, readable-sentence,
indexable-terms, negative?, rests-on, query-contexts, assertable-strengths,
sort-by-content, levels, calculi), the bootstrap fns (open-kb, clear!), the
in-process write-hazards and the process-wide switch-value take no target and
delegate to vaelii.core. They are here so a caller can require this one namespace
and reach the whole surface it needs.
Reads — including check / check-edit, which answer what assert would refuse
and write nothing — plus the four writes the browser performs: edit (an
assert/retract batch in one settle), edit-with-consequences (the same batch, plus
the belief it moved), forward-chain, and preview (which stores nothing but applies
a batch and rolls it back, so it holds the single writer). A remote result is already
EDN-clean (the daemon projects sentex records to maps); a local result is the raw
record, and both answer to the same keys, so a caller handles them identically.
How a *read* client reaches a KB — directly in-process, or over the daemon HTTP API
(`vaelii.host.serve`). It re-exports the slice of the `vaelii.core` read surface the
browser uses, so the browser is written once against these names and runs unchanged
against a local KB or a remote daemon.
A **target** is either a real KB (treated as local) or an access value from `local` /
`remote`. A KB-read op dispatches on it:
:remote → the client (`vaelii.host.client/call`) — one HTTP round-trip
:local → `vaelii.core`, via `serve/ops` (the very allowlist the daemon serves, so
local and remote answer through the same table and cannot drift)
a raw KB → the same local path (so a caller holding a plain KB needs no wrapper)
The pure display fns (`term-role`, `reified-term?`, `readable-sentence`,
`indexable-terms`, `negative?`, `rests-on`, `query-contexts`, `assertable-strengths`,
`sort-by-content`, `levels`, `calculi`), the bootstrap fns (`open-kb`, `clear!`), the
in-process `write-hazards` and the process-wide `switch-value` take no target and
delegate to `vaelii.core`. They are here so a caller can require this one namespace
and reach the whole surface it needs.
Reads — including `check` / `check-edit`, which answer what `assert` would refuse
and write nothing — plus the four writes the browser performs: `edit` (an
assert/retract batch in one settle), `edit-with-consequences` (the same batch, plus
the belief it moved), `forward-chain`, and `preview` (which stores nothing but applies
a batch and rolls it back, so it holds the single writer). A remote result is already
EDN-clean (the daemon projects sentex records to maps); a local result is the raw
record, and both answer to the same keys, so a caller handles them identically.What knowledge bases this process can load, and the lifecycle of loading one.
Everything above the engine assumes it is holding the KB. A browser that lists the KBs available, loads one while you watch, and switches to it needs two things the engine does not have: a description of a KB that has not been loaded yet, and somewhere for a load that takes minutes to run while the pages keep answering. Those are the two halves here.
A source is a KB you could load, as data — a kind, a name, and wherever the content comes from. Six kinds:
| kind | content | loader |
|---|---|---|
:core | the CxCore vocabulary head alone | vaelii.host.core-context |
:starter | the shipped schema-only ontology | vaelii.host.starter |
:generated | synthesized from numbers — types, rules, a fwd mix | vaelii.host.io.generate |
:corpus | a translated sentence corpus (OpenCyc) | a foreign reader, :cyc-corpus |
:dump | a vaelii export dump | vaelii.impl.io.import |
:store | an on-disk KB already in vaelii's own format | opened in place |
The first three ship in this repo and are always offered. The last three are found:
each directory on the search path (VAELII_KB_PATH, else ./kbs and ~/.vaelii/kbs)
is probed, and what marks it — a corpus meta.edn, a dump meta.edn, a records/ +
index/ pair — decides its kind. A catalog.edn (VAELII_KB_CATALOG, else
~/.vaelii/catalog.edn) names sources outside the search path. Nothing about a
machine's paths is baked into the repo.
An entry is a source that has been loaded, or is loading: a KB, a status, and a
progress reading the loaders report into (:on-progress, reported by every loader —
the corpus reader, io.import/import-dump and io.generate/load-into). The running
half of that is not here: a load is a job (vaelii.browser.jobs), which is what gives
it a thread of its own, the progress reading, the cancel flag and the report — so an
entry carries the job's id and reads its status rather than keeping one. One load runs
at a time, since a load claims this process's writer, and cancelling one is cooperative:
the loaders have no other safe interruption point, and an import is not a transaction,
so a cancelled load leaves the KB holding what had already landed.
A KB is readable before it is finished. activate asks only that an entry hold a
KB, so the one arriving can be the one every page reads — a corpus is browsable from
its first thousand sentexes, and a store that opens in seconds is browsable while
belief is still being rebuilt behind it. What that costs a reader is completeness, not
correctness, and active-caveat is what says so.
And a KB can go back out. export-entry! writes a loaded one as an export dump as
a job like any other, which closes the loop: a dump written under the search path is a
:dump source the moment its meta.edn lands, so exporting and reloading needs nothing
outside this namespace.
Unloading never deletes an on-disk KB. A memory-backed entry has its stores cleared (they would otherwise hold the corpus for the life of the JVM); a disk-backed one is closed — the file lock released, the directory left exactly as it was. The same directory can then be loaded again, or opened by another process.
What knowledge bases this process can load, and the lifecycle of loading one. Everything above the engine assumes it is holding *the* KB. A browser that lists the KBs available, loads one while you watch, and switches to it needs two things the engine does not have: a description of a KB that has not been loaded yet, and somewhere for a load that takes minutes to run while the pages keep answering. Those are the two halves here. **A source** is a KB you could load, as data — a kind, a name, and wherever the content comes from. Six kinds: | kind | content | loader | |--------------|-----------------------------------------------------|--------| | `:core` | the CxCore vocabulary head alone | `vaelii.host.core-context` | | `:starter` | the shipped schema-only ontology | `vaelii.host.starter` | | `:generated` | synthesized from numbers — types, rules, a fwd mix | `vaelii.host.io.generate` | | `:corpus` | a translated sentence corpus (OpenCyc) | a foreign reader, `:cyc-corpus` | | `:dump` | a vaelii export dump | `vaelii.impl.io.import` | | `:store` | an on-disk KB already in vaelii's own format | opened in place | The first three ship in this repo and are always offered. The last three are **found**: each directory on the search path (`VAELII_KB_PATH`, else `./kbs` and `~/.vaelii/kbs`) is probed, and what marks it — a corpus `meta.edn`, a dump `meta.edn`, a `records/` + `index/` pair — decides its kind. A `catalog.edn` (`VAELII_KB_CATALOG`, else `~/.vaelii/catalog.edn`) names sources outside the search path. Nothing about a machine's paths is baked into the repo. **An entry** is a source that has been loaded, or is loading: a KB, a status, and a progress reading the loaders report into (`:on-progress`, reported by every loader — the corpus reader, `io.import/import-dump` and `io.generate/load-into`). The running half of that is not here: a load is a **job** (`vaelii.browser.jobs`), which is what gives it a thread of its own, the progress reading, the cancel flag and the report — so an entry carries the job's id and reads its status rather than keeping one. One load runs at a time, since a load claims this process's writer, and cancelling one is cooperative: the loaders have no other safe interruption point, and an import is not a transaction, so a cancelled load leaves the KB holding what had already landed. **A KB is readable before it is finished.** `activate` asks only that an entry hold a KB, so the one arriving can be the one every page reads — a corpus is browsable from its first thousand sentexes, and a store that opens in seconds is browsable while belief is still being rebuilt behind it. What that costs a reader is completeness, not correctness, and `active-caveat` is what says so. **And a KB can go back out.** `export-entry!` writes a loaded one as an export dump as a job like any other, which closes the loop: a dump written under the search path is a `:dump` source the moment its `meta.edn` lands, so exporting and reloading needs nothing outside this namespace. **Unloading never deletes an on-disk KB.** A memory-backed entry has its stores cleared (they would otherwise hold the corpus for the life of the JVM); a disk-backed one is *closed* — the file lock released, the directory left exactly as it was. The same directory can then be loaded again, or opened by another process.
Worked examples of the reasoning the shipped ontology actually does — the data, and the one function that runs one.
Nothing here is a story about the engine. Each example names the sentexes it
rests on, and those are looked up in the live KB before anything is claimed: an
example whose rests-on sentences are not stored is reported unavailable rather
than answered, so switching to another corpus greys the examples out instead of
silently showing a verdict computed from vocabulary that is not there. The verdict
itself is an ordinary ask / escalate / check, and the proof is why.
Two kinds, and the split is about what the KB ships rather than about presentation:
read-only — no premises. The shipped schema is types, taxonomy, metadata and
rules, so everything asked of kinds is answerable with no write at all, and the
page computes it on render. This is where the taxonomy, transitiveInArg,
disjointness and the predicate meta-ontology live.
sandboxed — premises naming individuals. The starter ships no cast (the fables and their casts live in the test-world), so an example about defaults, joins or refusals has to bring its own, and it writes them into the reader's own sandbox context. Nothing shipped can see in, and the sandbox reset takes the whole thing away.
:expect is what the ontology is supposed to answer, and examples_test asserts
every one of them — so the page cannot drift away from the KB it describes.
Worked examples of the reasoning the shipped ontology actually does — the data, and the one function that runs one. **Nothing here is a story about the engine.** Each example names the sentexes it rests on, and those are looked up in the live KB before anything is claimed: an example whose `rests-on` sentences are not stored is reported *unavailable* rather than answered, so switching to another corpus greys the examples out instead of silently showing a verdict computed from vocabulary that is not there. The verdict itself is an ordinary `ask` / `escalate` / `check`, and the proof is `why`. Two kinds, and the split is about what the KB ships rather than about presentation: **read-only** — no premises. The shipped schema is types, taxonomy, metadata and rules, so everything asked *of kinds* is answerable with no write at all, and the page computes it on render. This is where the taxonomy, `transitiveInArg`, disjointness and the predicate meta-ontology live. **sandboxed** — premises naming individuals. The starter ships no cast (the fables and their casts live in the test-world), so an example about defaults, joins or refusals has to bring its own, and it writes them into the reader's own sandbox context. Nothing shipped can see in, and the sandbox reset takes the whole thing away. `:expect` is what the ontology is supposed to answer, and `examples_test` asserts every one of them — so the page cannot drift away from the KB it describes.
Long work, as jobs: one registry, one progress reading, one cancel.
Three things this process does take minutes rather than milliseconds — filling a KB from a corpus, writing one back out, and joining every rule over everything stored. Each of them wants the same four capabilities, and they are the only four: run on a thread of its own so the pages keep answering, say where it has got to, stop when asked, and leave a report somebody can read afterwards. That shape is here once.
A job is {:id :label :kind :status :progress :started :finished :error :summary :result-url}, plus a cancel flag and the future, which no view carries. submit
returns the id; job reads one; jobs lists them, newest first. The caller's work
is handed a progress! fn and nothing else: what it records shows up under
:progress, and what it throws is how cancellation lands, because a tight assert
loop has no other point at which stopping is safe.
One status vocabulary, whatever the job is doing:
:running → :cancelling → :done | :cancelled | :failed
:cancelling is the honest middle: cancel! sets the flag and returns, and the work
keeps running until it reaches its next progress report — which, for a phase that
reports none (opening a large store scans its whole record log before it says
anything), can be a while.
The single writer stays single. :writes names the KB a job writes, or true for
one it has not opened yet, and one writing job runs at a time: a second is refused
with a message naming the job that holds the writer. Two interleaved writers are not
serializable (docs/storage.md, the single-writer contract), and a registry that let two
through would be a way around the contract rather than a place to watch it from.
writes-kb? is the other half of the same question, asked by identity, so a job filling
one KB never blocks a write to another.
Cancellation is cooperative, and for a KB-writing job that is not negotiable. A
thread interrupt landing mid-cascade on a durable store surfaces as
ClosedByInterruptException and can leave a torn removal, so a job with :writes is
flagged and never interrupted however long it takes to notice. A job that writes
nothing may say :interruptible? true and be cancelled the hard way as well; the
registry checks both, so the two can never be confused for one another. A job's thread
is the pool's once its body has unwound, so the hard tier is fenced: the body
publishes :released under the job's :monitor and cancel! re-reads it there, which
is what stops an interrupt aimed at a job that has already finished from landing on
whatever the pool runs next.
A finished job's report outlives the job, for an hour — long enough to read what it did, since the page that would have shown it is usually the page you navigated away from. Nothing unsettled is ever dropped, at any age: forgetting a job is releasing its writer claim, and a thread that is still running is still writing. So a wedged job keeps its place and keeps counting towards the running badge, which is the truth about the process — better than a store two writers took turns on.
Long work, as jobs: one registry, one progress reading, one cancel.
Three things this process does take minutes rather than milliseconds — filling a KB
from a corpus, writing one back out, and joining every rule over everything stored.
Each of them wants the same four capabilities, and they are the only four: run on a
thread of its own so the pages keep answering, say where it has got to, stop when
asked, and leave a report somebody can read afterwards. That shape is here once.
**A job** is `{:id :label :kind :status :progress :started :finished :error :summary
:result-url}`, plus a cancel flag and the future, which no view carries. `submit`
returns the id; `job` reads one; `jobs` lists them, newest first. The caller's `work`
is handed a `progress!` fn and nothing else: what it records shows up under
`:progress`, and what it *throws* is how cancellation lands, because a tight assert
loop has no other point at which stopping is safe.
**One status vocabulary**, whatever the job is doing:
:running → :cancelling → :done | :cancelled | :failed
`:cancelling` is the honest middle: `cancel!` sets the flag and returns, and the work
keeps running until it reaches its next progress report — which, for a phase that
reports none (opening a large store scans its whole record log before it says
anything), can be a while.
**The single writer stays single.** `:writes` names the KB a job writes, or `true` for
one it has not opened yet, and **one writing job runs at a time**: a second is refused
with a message naming the job that holds the writer. Two interleaved writers are not
serializable (docs/storage.md, the single-writer contract), and a registry that let two
through would be a way around the contract rather than a place to watch it from.
`writes-kb?` is the other half of the same question, asked by identity, so a job filling
one KB never blocks a write to another.
**Cancellation is cooperative, and for a KB-writing job that is not negotiable.** A
thread interrupt landing mid-cascade on a durable store surfaces as
`ClosedByInterruptException` and can leave a torn removal, so a job with `:writes` is
flagged and never interrupted however long it takes to notice. A job that writes
nothing may say `:interruptible? true` and be cancelled the hard way as well; the
registry checks both, so the two can never be confused for one another. A job's thread
is the *pool's* once its body has unwound, so the hard tier is fenced: the body
publishes `:released` under the job's `:monitor` and `cancel!` re-reads it there, which
is what stops an interrupt aimed at a job that has already finished from landing on
whatever the pool runs next.
**A finished job's report outlives the job**, for an hour — long enough to read what it
did, since the page that would have shown it is usually the page you navigated away
from. Nothing *unsettled* is ever dropped, at any age: forgetting a job is releasing
its writer claim, and a thread that is still running is still writing. So a wedged job
keeps its place and keeps counting towards the running badge, which is the truth about
the process — better than a store two writers took turns on.The development browser's source reloader, which only scripts/start-vaelii-dev.sh
turns on (vaelii.browser.web/dev-repl). Before each request it reloads every watched
source file that changed since the last request, together with every loaded namespace
that requires one of them, transitively, in dependency order.
A protocol, record, type or interface is never redefined. Re-evaluating a
defprotocol defines a new interface and empties the protocol's extension map, and
re-evaluating a defrecord, deftype or definterface defines a new class, so a KB
loaded before the reload would hold values the reloaded code no longer recognizes. A
reload therefore loads a file form by form and leaves out each of those forms whose
protocol or class already exists; every other form is evaluated. A record keeps its
methods inline, where protocol dispatch is a direct interface call, and a method that
calls a function reaches the reloaded function through its var. An edit inside such a
form is not loaded: restart-owed names it until the process restarts, and the page
shows that list. A form for a class that does not exist yet is evaluated.
A held namespace is never re-evaluated at all. A namespace whose ns symbol carries
:clojure.tools.namespace.repl/load false holds protocols and method-less records
(vaelii.impl.types.*, vaelii.impl.protocols, …). An edit to one reloads nothing on
its account, and restart-owed names it.
Form by form, not tools.namespace's refresh. refresh removes each namespace with
remove-ns before it loads the file again, which re-evaluates every defonce in it and
empties the state a loaded KB keeps there: the cache registry, the change feed's
listeners, the thaw guard's installed readers. Loading into the existing namespace
keeps a defonce's value, as require :reload does.
Only loaded namespaces reload. A dependent this process never loaded, such as an LLM provider the browser has not reached, stays unloaded: loading it would run code the running server does not use.
tools.namespace ships in the :dev profile only, so its dependency graph and ns-form
reader are resolved when wrap-reload builds the reloader, never at this namespace's
load. The standalone jar compiles this namespace and has no tools.namespace.
The development browser's source reloader, which only `scripts/start-vaelii-dev.sh` turns on (`vaelii.browser.web/dev-repl`). Before each request it reloads every watched source file that changed since the last request, together with every loaded namespace that requires one of them, transitively, in dependency order. **A protocol, record, type or interface is never redefined.** Re-evaluating a `defprotocol` defines a new interface and empties the protocol's extension map, and re-evaluating a `defrecord`, `deftype` or `definterface` defines a new class, so a KB loaded before the reload would hold values the reloaded code no longer recognizes. A reload therefore loads a file form by form and leaves out each of those forms whose protocol or class already exists; every other form is evaluated. A record keeps its methods inline, where protocol dispatch is a direct interface call, and a method that calls a function reaches the reloaded function through its var. An edit inside such a form is not loaded: `restart-owed` names it until the process restarts, and the page shows that list. A form for a class that does not exist yet is evaluated. **A held namespace is never re-evaluated at all.** A namespace whose ns symbol carries `:clojure.tools.namespace.repl/load false` holds protocols and method-less records (`vaelii.impl.types.*`, `vaelii.impl.protocols`, …). An edit to one reloads nothing on its account, and `restart-owed` names it. **Form by form, not tools.namespace's `refresh`.** `refresh` removes each namespace with `remove-ns` before it loads the file again, which re-evaluates every `defonce` in it and empties the state a loaded KB keeps there: the cache registry, the change feed's listeners, the thaw guard's installed readers. Loading into the existing namespace keeps a `defonce`'s value, as `require :reload` does. **Only loaded namespaces reload.** A dependent this process never loaded, such as an LLM provider the browser has not reached, stays unloaded: loading it would run code the running server does not use. tools.namespace ships in the `:dev` profile only, so its dependency graph and ns-form reader are resolved when `wrap-reload` builds the reloader, never at this namespace's load. The standalone jar compiles this namespace and has no tools.namespace.
Somewhere safe to be wrong.
A sandbox is a scratch context of one browser session's own, hung below
CxWell so it sees the whole shipped ontology and nothing shipped sees it. A
reader can therefore use every type, every relation and every rule the KB ships, and
cannot damage any of them: their content is visible only from inside, and one control
takes all of it away again.
Why that shape and not a permission system: visibility here is logical, not
administrative. genlCx already decides what a context can see, and hanging the
sandbox at the bottom of the spindle gives exactly the asymmetry wanted — everything
flows in, nothing flows out — with no new concept and nothing to enforce. A shipped
rule firing over sandbox facts places its conclusion in the sandbox, because
placement is the maximal common descendant of the rule's context and the antecedents'
(docs/contexts.md), and the sandbox is the only context below both. So the derived
content is inside the thing that gets discarded, without anything arranging for that.
Three facts about the lifecycle:
genlCx edge per idle visitor.wrap-session and validated on the way back in —
a name is being built from it, and a name built from unvalidated client input is an
injection.edit's :remove, and the genlCx edge with them. The edge is not in the
extent — genlCx is a forced-decontextualized predicate, so it is stored in
CxUniverse — which is why it is fetched by hand rather than swept up with the
rest.Promotion — moving something out of a sandbox into a context that outlives it — is deliberately not here. A sandbox is a dead end, and a dead end that cannot be half-escaped is easier to reason about than one with an entry point in it.
Somewhere safe to be wrong. A **sandbox** is a scratch context of one browser session's own, hung below `CxWell` so it sees the whole shipped ontology and nothing shipped sees it. A reader can therefore use every type, every relation and every rule the KB ships, and cannot damage any of them: their content is visible only from inside, and one control takes all of it away again. Why that shape and not a permission system: visibility here is *logical*, not administrative. `genlCx` already decides what a context can see, and hanging the sandbox at the bottom of the spindle gives exactly the asymmetry wanted — everything flows in, nothing flows out — with no new concept and nothing to enforce. A shipped rule firing over sandbox facts places its conclusion **in the sandbox**, because placement is the maximal common descendant of the rule's context and the antecedents' (docs/contexts.md), and the sandbox is the only context below both. So the derived content is inside the thing that gets discarded, without anything arranging for that. Three facts about the lifecycle: - **The context is created on the first write, not on the first page.** A reader who only looks costs the KB nothing, and a KB full of empty sandboxes would be a KB with a `genlCx` edge per idle visitor. - **The session id is in the context name**, so two readers of one process never share one. It is minted into a cookie by `wrap-session` and validated on the way back in — a name is being built from it, and a name built from unvalidated client input is an injection. - **Reset is a real teardown**, not a flag: every sentex in the extent goes through `edit`'s `:remove`, and the `genlCx` edge with them. The edge is not in the extent — `genlCx` is a forced-decontextualized predicate, so it is stored in `CxUniverse` — which is why it is fetched by hand rather than swept up with the rest. Promotion — moving something out of a sandbox into a context that outlives it — is deliberately not here. A sandbox is a dead end, and a dead end that cannot be half-escaped is easier to reason about than one with an entry point in it.
The inline-SVG primitives the term page's concept graph is drawn with: a node, an edge, an arrowhead, and the arithmetic that lays out a row, a column or a ring.
No graph library. The browser ships two JavaScript files and this adds none — a layout that is a fold over a row of boxes is a dozen lines, and a dependency that drew it would be the largest thing the client loads. Nor a shell-out: a page that renders by starting a process is a page that cannot be served.
Everything here is pure — no KB, no access facade, no belief — so it is tested on hand-built maps. What a node means is the caller's: it supplies the term, the colour class, the link and the tooltip, and this decides only where the box goes and what shape it is.
Coordinates live in one flat user space and may be negative; scene crops the
viewBox to the union of what was actually drawn, so a sparse graph is centred rather
than adrift in a fixed canvas and a long snake_case label is never clipped. Every
number reaching an attribute is a long: Clojure's / yields a ratio, and 1/2 in
an SVG attribute is not a coordinate.
The inline-SVG primitives the term page's concept graph is drawn with: a node, an edge, an arrowhead, and the arithmetic that lays out a row, a column or a ring. **No graph library.** The browser ships two JavaScript files and this adds none — a layout that is a fold over a row of boxes is a dozen lines, and a dependency that drew it would be the largest thing the client loads. Nor a shell-out: a page that renders by starting a process is a page that cannot be served. Everything here is **pure** — no KB, no access facade, no belief — so it is tested on hand-built maps. What a node *means* is the caller's: it supplies the term, the colour class, the link and the tooltip, and this decides only where the box goes and what shape it is. Coordinates live in one flat user space and may be negative; `scene` crops the `viewBox` to the union of what was actually drawn, so a sparse graph is centred rather than adrift in a fixed canvas and a long snake_case label is never clipped. Every number reaching an attribute is a **long**: Clojure's `/` yields a ratio, and `1/2` in an SVG attribute is not a coordinate.
A small reitit-ring web browser over a KB:
/ the upper ontology (contexts, types, core predicates, disjointness) /stats KB-wide counts, contexts by size, and the reasoning-health ledgers /find?q=<pattern> the terms whose name matches, from the index's term roster /term?q=<term> every sentex containing the term, grouped by the index root that reaches it (functor / argument-position / context / term-index) /sentex/:id a sentex (literal or rule): its belief state (IN, or the why-not reason — superseded / defeated / unsupported), supports, dependents /justification/:id a justification: its supports (arguments) and dependent sentex /levels?q=<goal> the lookup-to-query stack: what each of the 8 levels answers /edit the multi-sentex editor (GET seeds it, POST saves) — a fragment /{term,find,levels}/rows one more page of a capped list, as bare rows
Run it with lein run -m vaelii.browser.web (serves a starter-loaded KB on :3000).
Handlers are pure request -> response, so they are testable without a server.
Every page is answered twice over: as a whole document, and — when htmx asks, which
is every navigation and search — as the #main fragment that actually lands. What a
page costs in KB reads is part of what this demonstrates, since the browser reads the
public surface alone and each read is a round-trip under --attach; see the view
section below and docs/web.md.
A small reitit-ring web browser over a KB:
/ the upper ontology (contexts, types, core predicates, disjointness)
/stats KB-wide counts, contexts by size, and the reasoning-health ledgers
/find?q=<pattern> the terms whose name matches, from the index's term roster
/term?q=<term> every sentex containing the term, grouped by the index root that
reaches it (functor / argument-position / context / term-index)
/sentex/:id a sentex (literal or rule): its belief state (IN, or the why-not
reason — superseded / defeated / unsupported), supports, dependents
/justification/:id a justification: its supports (arguments) and dependent sentex
/levels?q=<goal> the lookup-to-query stack: what each of the 8 levels answers
/edit the multi-sentex editor (GET seeds it, POST saves) — a fragment
/{term,find,levels}/rows one more page of a capped list, as bare rows
Run it with `lein run -m vaelii.browser.web` (serves a starter-loaded KB on :3000).
Handlers are pure `request -> response`, so they are testable without a server.
Every page is answered twice over: as a whole document, and — when htmx asks, which
is every navigation and search — as the `#main` fragment that actually lands. What a
page costs in KB reads is part of what this demonstrates, since the browser reads the
public surface alone and each read is a round-trip under `--attach`; see the `view`
section below and docs/web.md.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |