lite serve — the repo browser over HTTP, no CommonMark yetThe lite verbs already produce everything a repo browser shows — list, log, commit, diff, cat, tree, blob — but only the terminal can see it: view/ spells dog tok tags as ANSI SGR through theme.js. This ticket adds lite serve: an HTTP server on quickjab's net/pol loop (QJAB-004 parses the requests), a router mapping URLs onto the existing index/ verbs, and an HTML twin of the painter — the same tok tags become <span class>, the same theme palette becomes CSS. Markdown stays UNRENDERED here: .md/.mkd serve as syntax-painted source like any file; the CommonMark/StrictMark render (LITE-030, LITE-031, be/verbs/mark/render.js) is a follow-up ticket, not this one.
Transport and content both exist; only the emitter and the glue are missing.
quickjab/net.c + pol.c: Node-style net.createServer, one implicit event loop, proven by quickjab/test/net.js echo + 200K-payload legs.dog/abc/HTTP.h parse/compose leaves; keep-alive and body handling are the JS caller's.index/ verbs (list, log, commit, diff, cat, tree, blob) return structured rows off the .git/be/ lane + ODB; view/theme.js is the ONE place SGR is spelled, tagged by tok letters.lite serve in a git repo answers a browser with the same views the pager shows, links intact.
/list/<path>, /log/<path|hex>, /commit/<hex>, /diff/<hex|path>, /cat/<path>?<rev>, /tree/…, /blob/<hexlet>; / is the root list. The router is arg-blind — it hands (verb, arg) over, verbs resolve.<span class="tok-X">, theme.js palette → one generated stylesheet; diff hunks and list/log rows keep their current column layout.mark/ stays untouched.theme.js data, never a second palette; tok recognition stays the tokenizer's.test/serve/run.sh under dash, $LITEJAB picks the runtime, fixtures under $HOME/tmp.--port overrides./cat/<ref> pass-through, no serve-side resolverhttp._drain/http._feed are staged in
///quickjab (http.c, test/http.js); built against them directly.Built in //LITE-034, staged, not committed. Based on ///lite#4cf63f4a; an
EXTERNAL get moved the worktree onto ///lite#3b0fdb24 (LITE-031's GFM) mid-run
— none of the nine staged files is a mark/ one, and the whole suite (that
ticket's own new test/mark/gfm leg, 49 checks, included) was re-run green on
the new base.
The first path segment names the verb, everything after it is the ARG, verbatim
— ?<rev> rides across as the URL's own query. index/serve.js ROUTE is the
whole table; urlOf/argUrl is the same table backwards (a pager click target
-> its URL), so a ruling on the scheme is an edit in that one file.
/ list the root list /list/<path>?<rev> list /log/<path|hex> log /commit/<hex> commit /diff/<hex|path> diff /cat/<path>?<rev> cat /tree/<path|hex> tree /blob/<hexlet> blob /style.css the ONE generated stylesheet anything else 404, in the verb's own plain words
A pager target that is NOT a <verb> <arg> line is a REFERENCE, and a reference
is RESOLVED WHILE THE PAGE IS PAINTED (ruling 2026-08-15): the emitted href is
already final.
<path> -> /cat/<resolved path>#b<landed token byte> <path>:<line>:<col> -> the same, anchored on the line's first token
(or the column's own token)
<path>:<off>:<hashlet> -> index/perma.js follows it; the anchor is the
token's byte TODAY, deleted-line case included
a directory -> /list/<resolved path>/ nothing / SEVERAL -> NO href: plain painted text, never a 404 link
The resolution is main.js seatOf — the pager's own door, split out of its
opening so both callers share one mechanism and one fallback order (permalink
follow, else the path the fs answers, else the LITE-011 FSEG partial, else the
LITE-024 worktree scan). lite serve owns no resolver.
index/serve.js — 293 lines, new. net.createServer on the implicit pol
loop, the ROUTE table, the link builder, the responder. EVERY QJAB-004
call sits in drainHead/feedHead — two one-line functions — so an API
change is edited in one spot. Reads only: GET and HEAD answer, every other
method is 405, no request body is ever read, the listener binds 127.0.0.1
and no flag opens that up. Default port 8034, --port <n> overrides; the
repo is found once and the cwd moves to its root, so every URL and every
verb resolve against the same frame. A malformed request URI, a refused
head and a throwing verb are 404/400/500 pages, never a dead loop.
Reference resolution is refResolve: it calls the door, spells the answer
as a URL through index/read.js's OWN path gate (so a ref outside the repo
simply gets no page), and anchors on the landed token. One page = one
cache plus one budget: a repeated reference is followed once, and past
REF_CAP (512 distinct) a reference paints plain rather than holding the
single-threaded loop.view/html.js — 181 lines, new. The HTML twin of the ANSI painter over the
SAME hunks: tok32 tag -> <span class="tok-X">, the diff side bits ->
side-in/side-rm, a hidden U span -> the <a href> of the span in
front of it (view/pager.js _targetAt's own rule), an F token with no U
-> a reference (LITE-015), linked only if it RESOLVES. Every token span
carries id="b<start byte>" — the token's own offset identity, the one
index/perma.js walks — so #b<off> names a token and a :target rule marks
the landed one in the theme's band. A page's second and later hunks prefix
the ordinal (b1-<off>), so ids stay unique. A hunk body is a <pre>, so
list/log columns and diff hunks keep the terminal's layout to the byte.
Source markup is escaped; a hunk with no toks still paints its bytes.main.js — the DOOR SPLIT (the fix for the raw-/cat/<ref> bug): seatOf
is the resolution — permalink follow, path, FSEG partial, worktree scan —
lifted out of openTarget/openPerma/openPartial, which now only OPEN
what it names, and statOf is the fs probe both share. Net -13 lines: the
two openers folded into one, no logic duplicated anywhere.view/bro.js — +33 lines: landAt (a 1-based line:col -> the byte, and
whether the COLUMN named one) and tokSpanAt (the token covering a byte),
lifted VERBATIM out of view/pager.js _land/_landTok, which now call
them. The pager scrolls by this math and the HTML painter anchors by it,
so the browser lands on the token the pager would select.view/pager.js — -14 lines, its own copies of the two replaced by the
bro.js calls; behaviour unchanged (click/land/refline/perma/cursor legs
green before and after).view/theme.js — +16 lines. Additive only, no ANSI behaviour touched: the
four slots the C table has and this file lacked (W, E, X, Q — the
body slot and list's STATUS-verb markers), plus the banner band and the
two diff wash slots as RAW SGR PARAMETERS next to the ESC-wrapped
spellings. The HTML painter reads THIS module and nothing else: one
palette, one stylesheet, and a theme swap ($BRO_THEME) repaints the
browser exactly as it repaints the pager.main.js — also runServe plus its dispatch line. It hands the whole DOOR
(the VERBS table AND seatOf/statOf/openPath) INTO serve.js rather
than serve.js requiring main.js back: one mechanism, no import cycle.README.mkd — +1 line, the usage entry after blob.mark/ untouched; no CommonMark, no StrictMark, no site chrome — a
.md/.mkd serves as syntax-painted source like any other file. ZERO
edits to any quickjab tree.test/serve/run.sh — 341 lines, new; test/serve/url.js — 291 lines, new.
*PASS serve 174 checks* (91 over the wire + the 83-check headless
leg). The shell leg builds a THREE-commit git fixture under $HOME/tmp (a
subdir, an uncommitted edit, a .mkd carrying a path reference and a ticket
code, a name with a SPACE in it, and a fixed-width target.c so every byte
offset below is stated arithmetic, not a mined number), refuses five bad
invocations in the foreground, then starts ONE server and walks every URL
asserting the status line, the content type and the paint: the tok spans and
their byte-offset ids, the <a href> targets, the side-in/side-rm wash,
the generated .tok-* and :target rules, POST/PUT/DELETE refused with the
worktree provably unmoved, HEAD answering GET's own length.
The REFERENCE leg is the bug's own: target.c is seeded with six 13-byte
lines, then two lines are prepended and MARK002 is deleted, and two
permalinks are minted against the FIRST blob (index/perma.js's own packing, so
the test mints no format itself). A ref.c carrying all four forms is served
and the RENDERED hrefs asserted:
target.c:6 -> /cat/target.c#b63 the line's first token target.c:<off>:<hl> -> /cat/target.c#b54 MARK004, moved down two lines target.c:<off>:<hl> -> /cat/target.c#b37 MARK002's line, now deleted nosuch.c:3 -> no href at all, still painted as source
then /cat/target.c is fetched and each of those three anchors asserted to
name a REAL token of it (id="b54">MARK004, id="b37">int, id="b63">int),
plus "no raw reference reaches a url" (no /cat/target.c: and no %3A
anywhere) and "exactly three resolvable references linked".
url.js is the headless half — the URL table both ways INCLUDING a round
trip, theme.js -> CSS, the shared landing math (landAt/tokSpanAt over a
known three-line text), the painter over hand-built hunks, and every branch of
reference resolution driven through a STUB door: file:line, file:line:col, a
permalink's own token bytes, a directory, a miss, an ambiguity, a target
outside the repo, an untokenised target, the per-page cache (a repeat follows
once) and the budget (past it a reference paints plain).
All existing legs still pass, unchanged from the pre-work baseline: 21 of 23
PASS, and lite/diff (1 of 34) + lite/pager (1 of 33) fail identically
BEFORE and AFTER this work — both are environment artefacts of this box
($HOME is itself a git repo, which breaks diff's "a non-repository is
refused" leg; the pager leg loses pre-queued pty keys). Runtime: a quickjab
built from ///quickjab with -DJAB_JSRC=<this tree> -DQUICKJAB_JSRC_PACK=ON
($HOME/bin/lite carries the leaves but packs the OLD lite tree, so it cannot
run a modified one). A 220 KB source served as a 2.9 MB page intact, which is
the net write-FIFO growth path.
A ticket code resolves to no file (the door tries the path, then the FSEG
partial — a code is neither), so it renders as plain painted text. If a ticket
code should instead reach lindex <code>'s backlink suspects, that is one row
in ROUTE plus one branch in refResolve. An AMBIGUOUS reference (several
files answer) is likewise plain text: the pager shows a chooser there, and a
chooser page would need a route of its own.
Suggested commit message:
LITE-034: lite serve — the repo browser over HTTP