Skip to content
quarry Timeline
Login

Timeline

50 descendants and 50 ancestors of 69fc3f06d1e48a50

2026-08-25
21:43
Pin the derived/truth split so it cannot drift back

--data is derived and disposable; the OLTP is truth and is not. That is only worth stating if something checks it.

Wiping the derived directory must keep accounts and capability grants, and the converse test keeps the old interleaved layout honest: without the flag the same wipe destroys them, which is exactly what the split exists to escape.

Confirmed against the real binary too: wiping a 4.3M data directory leaves the forge serving 200 with its file listing intact and the index rebuilt from the stores, and the account still there. check-in: 6c2ed45467 user: dmestas tags: trunk

21:42
ops: tie a store call to the request that caused it

The panel could find a slow route and never say why: the render list and the subprocess list were two independent tables, so 'GET /search took 1132ms' and 'zit artifact ran 499 times' could not be connected even when one caused the other.

Every request is now tagged at the outermost middleware, and every store call records the tag. Work with no request behind it — ingest rounds, warm passes, the doc indexer — records the empty string, which is itself the useful distinction: store load with nothing to blame is background, and telling the two apart is most of what the subprocess panel is for.

Also fixes a silent hole of the same shape as the route-pattern bug: Repo was read with req.PathValue in the outermost middleware, where the router has not set it — the values live on the request the mux passes DOWN. Every render recorded until now carried an empty repo, 61 of 61 rows in the analytics store, which made the numbers unattributable on a fleet where repository size is the dominant cost. Read from the path instead. check-in: bc35cc0c45 user: dmestas tags: ops-drill

21:32
ops: a live stream is not a slow page

The fleet recorded GET /r/{repo}/timeline.sse at a p95 of 435 SECONDS. That is not a slow render, it is a connection somebody kept open for seven minutes — chat, the timeline and the ops panel all hold one for as long as the reader keeps the tab.

Two consequences, the second worse than the first. The slowest-routes panel was headed by four streams, burying GET /{$} at 1198ms against a 13ms median — the one real finding on the page. And every stream crosses the render budget by design, so the first-crossing alert fired on connections that were working perfectly: the muted-channel failure that rule exists to prevent, defeated from inside the same feature.

Detected from the response, not the path. A handler that streams says so in its Content-Type; a rule matching '.sse' would miss the next streaming route added under another name. Prefix-matched, so a charset does not defeat it.

Found by the panel on its first look at real traffic, which is the argument for having built it. closed check-in: cf9a277e13 user: dmestas tags: ops-streams

21:27
Separate forge truth from derived state: --oltp

--data holds two things that behave nothing alike. Per-repository indexes, built archives and the event store are DERIVED — rebuildable from the stores (0001 5), and therefore private to one node. oltp.db is not: accounts, capability grants, webhooks, praise, mirrors, saved views and CI runs are forge-local truth (0001 8), derivable from nothing.

Interleaved in one directory they could not be deployed differently, so two nodes could not have their own fast local index without also having two account databases that disagree. 256KB of the 4.5GB was the whole obstacle to running more than one node.

--oltp defaults to <data>/oltp.db, so a single-node deployment is unchanged. Every command that opens the OLTP takes it.

Verified: node A creates an account, node B with its own --data and the shared --oltp sees it; node B without the flag reports no accounts, which is the divergence this removes. Both nodes serve at once, each with its own index. check-in: 308751929f user: dmestas tags: trunk

21:09
Accepted. The forge now tells you, rather than waiting to be asked. check-in: 3f238c575b user: dmestas tags: trunk
21:08
Accepted. History, with the cgo cost taken knowingly and the wasm path verified intact. check-in: 61c32a8057 user: dmestas tags: trunk
21:08
Accepted. The guards go in before the work they guard. check-in: 547cb92c6f user: dmestas tags: trunk
19:29
tests: add the budget guards, which the last commit left behind

Same mistake as before: extras was printed and then the commit ran without the add. Caught by checking the check-in rather than the worktree, which is the only check that works. closed check-in: 63ad319ef2 user: dmestas tags: ops-budget

19:29
ops: the budget is one number, and it notifies

500ms was a literal in a template and a constant nowhere. The panel coloured a p95 over it and nothing else knew, so the forge could show a route in red and tell no one.

obs.Budget is now the single definition. The panel colours against it — sent on the wire rather than hardcoded, so the page and the threshold cannot disagree — and crossing it triggers a notification.

First crossing only, on errlog's rule and for errlog's reason: the same page is slow on every request until somebody fixes it, and an alert channel that repeats itself gets muted. A muted channel is worse than none, because it reads as healthy. The decision is made under the lock, so two slow requests racing on one route cannot both report it.

This is the difference between an operator watching the forge and the forge watching itself: the ops page has to be looked at; a notification arrives. check-in: fc8ef2165b user: dmestas tags: ops-budget

19:24
ops: history, not just a snapshot

The rings die with the process, and this fleet restarts constantly — a 4.24s render and a 546 load average both left no trace. obs.Sink had one implementation and it did nothing; this is the durable one.

DuckDB because the workload is columnar: append-only, time-ordered, aggregate-queried, never point-looked-up by a page. RFC 0001 defers the lake to 'when hydration and analytics — the things columnar is for — exist', and reserves the operational surface for SQLite. This is that analytics workload, not a second operational store. Wide flat tables, no keys and no indexes, which is also what a Parquet COPY TO wants when the lake arrives.

Isolated behind a build constraint, in its own package, imported only by the composition root. Both halves of the reason were measured rather than assumed: CGO_ENABLED=0 fails on go-duckdb's bindings, and GOOS=js reports 'build constraints exclude all Go files'. Leaving the import off the request path is not enough — any reachable import breaks a wasm build — so cmd/quarry has two files, one per side of the tag. Verified: GOOS=js GOARCH=wasm builds cmd/quarry clean at 76MB, without DuckDB.

Buffered, never a write per request, and flushed on a timer so a quiet forge still lands rows rather than holding them in memory for days.

Failure is not fatal. The first run failed on 'at' being reserved in DuckDB; the server started anyway, the panels worked, and the log said what was wrong. Analytics is derived data about a forge that works without it.

Costs, measured: +462MB module cache, binary 57MB -> 104MB, cold build +10s, and CGO_ENABLED=0 no longer builds the server. closed check-in: 564f272d3c user: dmestas tags: ops-history

19:05
tests: the regression guards that were never added

Seven test files were written this session, reported as landed, and never committed: `zit commit` with no paths captures TRACKED files, and new files need `zit add` first. make check passed every time because they were on disk in the worktree, which proves nothing about what is in the check-in.

What was missing:

- search kinds, held in agreement across four places (the index list, two ops validators, the scope chips, hitOf). This is the guard that caught `doc` and again `file` the moment each was added; without it the validator silently refused a kind the chips offered. - technote edits collapsing to one timeline row, folded in three different orders, because a clone delivers artifacts in wire order. - wiki and technote search matching the CURRENT revision only, the "terrified" case. - Contributors(0) meaning every contributor rather than a silent 100. - the ops phase states, the stalled-store trap, and an empty fleet.

Verified by removing them and running the suite: it stays green, which is precisely the problem — trunk has been passing on their absence. closed check-in: 24fdaa723a user: dmestas tags: search-guards

19:03
land ops-phase check-in: 96b30ad4f5 user: dmestas tags: trunk
19:02
Add internal/obs, which trunk imports but did not carry

Check-in 2cacce9b01aa added obs.Default.Spawned, RecordSubprocess, RecordRender and Snapshot calls across zitcli, web and api, but the package itself was never added — so trunk did not compile at all.

The files are the ones from the authoring session's tree, unmodified, tests included. Nothing here is my design; this only makes the import resolve so the tree builds again. check-in: 5229b05522 user: dmestas tags: trunk

17:37
ops: say what the forge is doing, not just how fast

The panels were steady-state numbers and said nothing while a fleet came up — which is the window an operator most wants to watch and the one where a forge most looks hung. A multi-repo re-ingest on this fleet was mistaken for a hang precisely because nothing said '23 of 25 stores ready' while it worked.

A phase banner leads the page: starting, indexing, syncing or rest, with stores ready over stores served, artifacts this session, the slowest recent round, and the names of what is rebuilding — which, not merely that something is.

Derived rather than tracked. Every store already reports Indexing, LastOK, LastCount, Total and Stalled; a phase field somewhere would be one more thing to keep true. The test ordering is the reader's priority: rebuilding outranks syncing, and a store that has never finished a round outranks both.

A stalled store is DONE waiting — it needs a person, not time — so it does not hold the fleet in 'starting' forever and does not sit in the bar as pending. Ready and stalled are drawn as two segments of one track, or the bar could never fill. closed check-in: 78fbef9d30 user: dmestas tags: ops-phase

14:58
Accepted. The forge can see itself; the rest is history storage. check-in: 2cacce9b01 user: dmestas tags: trunk
14:45
ops: the forge can see itself

quarry could not answer two questions about its own behaviour. A render time was printed in the page footer and discarded, so a 4.24s files page was visible only to whoever loaded it and read the footer. Subprocess load was visible nowhere: a warm loop once ran 134 concurrent `zit ls --long` against the same hash, load average 546 on 14 cores, and /fleet said "all healthy" throughout.

/fleet/ops, site-admin only. Slowest routes by p95 — a mean hides the tail, and the tail is the thing worth finding. Store subprocesses rolled up per verb, with the number that actually mattered: how many run at once, and the peak, which outlives the incident. Recent renders with ops time and the zit time inside it.

Instrumented at the two seams that cover everything: the ops registry, which every page's data crosses, and the outermost middleware, which is the only place the template's own cost is known — the footer's breakdown is assembled before the template runs, so a page can never report the time it spent rendering itself.

Datastar with no no-script path, deliberately: an internal dashboard that cannot move while an operator watches a fleet index is worth much less than one that can, and no consumer depends on it. The op behind it is registered and versioned like any other.

The rings are small and in memory. This is the snapshot; history belongs in the analytics store behind obs.Sink, which is an interface with a no-op default so the persistent implementation can be cgo without the request path importing it. closed check-in: 022966bc05 user: dmestas tags: fleet-ops

14:24
Ask for names when only names are used

zit ls --long adds size, permission and text/binary, and the last of those reads every listed blob. On a large store the annotated forms cost ~18ms per file where the plain two-field listing costs ~0.09ms: measured on tcl, 0.20s plain against 38s for --size and 41s for --long. The cost scales with the STORE, not the listing (filed as zit#148).

Four callers read only .Name and threw the rest away: the About marker scan, a root check-in's file set, the doc index, and CI's file list. They use LsNames now, which runs the plain listing and caches it beside the annotated one.

The tree page and its warm still need the columns they display, so they are unchanged; that cost is the upstream issue's to fix. check-in: 78db482057 user: dmestas tags: trunk

14:00
Warm the tip listing once per tip, not once per poll tick

The poll loop ran 'go cli.Ls(ctx, tip)' on EVERY tick as a bare goroutine whose result was discarded. On a store where that call takes seconds the next tick started another before the last finished, and across a fleet they compounded.

Measured: 134 concurrent 'zit ls --long', all against the SAME hash, under one server, load average 546 on a 14-core machine.

The listing is immutable per version, so it needs computing once per version. Now it skips when the tip has not moved and when a warm is already in flight. check-in: a3475428ac user: dmestas tags: trunk

13:50
One warm pass per repository at a time

warmReads and indexTree each spawn several zit subprocesses — ls, ls-under, a cat per file, an archive — and on a store tcl's size each takes seconds. They were launched as bare goroutines from every round that moved the tip, so the next round started another set before the last had finished and they stacked.

Measured on the demo fleet: 43 zit children under one server, three of them the same ls against the same hash, load average 546 on 14 cores.

A warm pass already running IS the warm pass. CompareAndSwap claims the slot and the winner clears it on the way out; a round that finds one in flight skips rather than piling on. check-in: 0325ba4a2c user: dmestas tags: trunk

13:04
Settling a landing is idempotent

Accepting an already-accepted landing wrote another state reply, and four stacked up on one record before it was noticed.

Two causes. The guard read state from the INDEX, which lags the store by an ingest round, so a second press inside that window saw 'open' and passed. And zit does not refuse the write either — an already-contained branch still gets a reply saying 'accepted', with 'nothing to merge' in the body.

So: fold first, re-read, then decide. A settle is deliberate and rare and can afford one ingest round to know what it is settling. And already-settled now succeeds writing nothing, because that is the end state the caller asked for; an error made a double press look broken when the landing was exactly as they wanted.

Proven against a running server: three accepts in a row leave one 'accepted this' reply and no 'nothing to merge'. check-in: f102c4c7ec user: dmestas tags: trunk

12:46
Suite green. Two fixes: page numbers centred (nav a is inline-flex, so text-align was inert) and segmented strips wrapping with gap dividers. check-in: 55fb3cfdec user: dmestas tags: trunk
12:45
Segmented strips wrap correctly: dividers are gaps

A border-right per item is the single-row idiom, and it breaks the moment the strip wraps — the item ending a row keeps a divider that lands against the container edge, the last item overall has none, and nothing rules between rows. Measured live at 390px the activity strip is three rows, so wrapping is the normal case.

Letting the container's colour show through a 1px gap draws every divider the layout actually needs, vertical and horizontal, with no rule needing to know where a row ends. closed check-in: 76dc2b8d96 user: dmestas tags: pager-shape

12:14
A page number is one shape, selected or not

The glyph was always centred; the boxes were not. The current page is a span and took 0.5rem of padding with no border, every other page is an anchor and took 0.6rem plus a 1px border — 48px against 53.2px for the same digit.

So the highlight changed size as the pointer moved along the row, and a number sitting still inside a box that grew around it read as though it had slid off centre. Padding and border are stated once for both now.

Measured: every cell 53.2x32.7 at desktop, 37.2 wide at 390px, glyph centred in all of them. check-in: 2ae6328950 user: dmestas tags: pager-shape

12:01
Suite green; merges clean. Three fixes: the events gate, the forum meta line, and the pager on narrow screens. check-in: 9c4f090ca6 user: dmestas tags: trunk
12:01
Pager fits a phone

The count is a statement about the list, not a control in it, so on a narrow screen it drops to its own line. The controls are trimmed to keep theirs: measured at 390px the row wanted 398px of a 358px container, and most of the excess was the number cells' 2rem min-width plus padding.

Before, whatever spilled — usually 'older' — landed centred under the numbers beside the count, because justify-content centres each wrapped line independently and it read as though it belonged to them. closed check-in: d42e59bdef user: dmestas tags: events-public

11:43
Forum meta line wraps as a sentence

It was a flex container with no flex-wrap, so every bare text run between the elements — ' · started ', ' · last reply ' — was its own anonymous flex item. On a narrow screen the default nowrap shrank each item until its text wrapped INSIDE it, stacking the words in columns instead of wrapping the line once.

Normal flow wraps it the way prose wraps. The name with its mark, and each date, are held together with nowrap so the break lands between them rather than through them. check-in: 968a59a2b0 user: dmestas tags: events-public

11:05
The event log is as public as the timeline

Every other repository page leaves visibility to the ops layer. A gateAt here made this the one page a public repository would not show an anonymous reader, while its timeline, forum and landings all did.

Safe because of what is on it. The derived events restate artifacts the timeline already shows. The operational ones carry no identity at all — Counted is func(repo string, clone bool), with no user and no address anywhere in the path — so a clone event says how many, never who. check-in: 318949277b user: dmestas tags: events-public

2026-08-24
23:12
land praise-ui check-in: 411e60c2d5 user: dmestas tags: trunk
23:05
Suite green; merges clean. Already deployed — the running binary carried this while the site was down, and trunk now matches it. check-in: d8a35694f0 user: dmestas tags: trunk
22:59
Bind the port without waiting on every repository

The forge could not answer for minutes after starting. Register runs before ListenAndServe and probes each store with 'zit submanifest show', and the note there said it costs ~250ms — measured on small stores. On tcl it is 116 SECONDS, on sqlite 16. A fleet with either in it held the whole site down.

The probe is bounded at 2s now. Past the deadline it is simply an unanswered question, which the existing false already handled: the fossil clone URL is withheld rather than offered wrongly.

Registration is also timed and logged, because each repository's cost is the whole forge's downtime and a slow one should be nameable rather than suspected. closed check-in: 65e953863a user: dmestas tags: startup-latency

22:17
merge search-current into trunk check-in: 37c9599a15 user: dmestas tags: trunk
22:17
search: match the current revision, not any revision

A wiki page and a technote are EDITED: every revision is its own artifact under one stable name. Keyed by artifact hash they piled up in search_doc, and Search's group-by then returned whichever revision matched BEST — so a query found text deleted years ago and linked to a page that no longer contains it. On the live fleet "terrified" matched spatialite_gui's wiki home, which has not said it since 2016; libfossil/home had 117 revisions all searchable.

One row per page now, holding the current revision, newest wins on date so a clone may deliver revisions in any order. Every revision stays readable in the page's own history, which is where the question 'what did this used to say' belongs.

Existing indexes are rebuilt from wiki_edit and technote at open rather than left to the fold: the fold only fires on artifacts that arrive after this code, so deleting the stale rows alone would empty wiki search until somebody happened to reindex. closed check-in: e262fc246c user: dmestas tags: search-current

21:40
merge hit-meta into trunk check-in: c4b5ef5d95 user: dmestas tags: trunk
21:40
search: results say what tells them apart

"use after free" on tk returned three rows identical on screen — same repository, same title, same snippet — because the same fix landed on three branches and the row carried nothing that differed. The only way to find out which one you wanted was to open all three.

Rows now carry the hash, date, author and branch for a check-in; the status and date for a ticket; the date and author for wiki, forum and technote. A doc or a file is identified by its path and gets nothing, rather than a date nobody asked for.

Decorated by primary key, for the page actually shown: the FTS query groups over a match set that can be thousands of rows, so joining there would decorate all of them to render fifty. On the fleet, decorated AFTER the merge trims — every repository is asked for the full limit so the merge can pick freely, and decorating before it meant 25 indexes times 50 rows to render 50.

One definition of the line, shared by both search pages. They are separate templates, which is why the first version of this reached only the per-repo one. closed check-in: 5cc9306b59 user: dmestas tags: hit-meta

21:20
Event log: proof the hooks fired

Read straight off the vocabulary stream — no table, no fold, nothing to keep in sync, so it cannot disagree with what the forge published.

The window is the stream's window, 24 hours, and the page says so: a log that let a reader assume history would be lying about a live tail.

Asks the consumer how much is pending before fetching. Without that a quiet repository waits out the fetch timeout, which would make 'nothing has happened yet' the slowest page on the site — 2s became 9ms.

Linked from the webhooks settings, not given a tab: that is where the question gets asked. closed check-in: a5c5fe771e user: dmestas tags: praise-ui

21:19
merge tree-search into trunk check-in: cd06e1345b user: dmestas tags: trunk
21:19
search: the tree is searchable — documentation and source

quarry rendered both as first-class surfaces (the docs tab, the file browser) and could find neither. Fossil searches documentation but not source; this does both, from one pass.

One tarball, not a read per file. cat per path costs a subprocess and a store open each: 2,199 files on sqlite is ~31s, against 2.2s for one archive — the tarball is the whole tree in one pass, which is the shape of the question.

Tip-only. Indexing every revision of every file would dwarf the rest of the index many times over to answer a question nobody asks; what a file used to say is a question its own history answers.

ref carries the version, not just the path: a link has to name the check-in the row was indexed at, and resolving a branch name at click time would be a second source of truth about which tree the row describes.

Binaries and files over 1MB are skipped and counted — a search that silently covers less than the tree beside it is the failure this change is about. closed check-in: 8df971b482 user: dmestas tags: tree-search

20:29
merge doc-reindex into trunk check-in: 1631ff8ecc user: dmestas tags: trunk
20:29
doc index: survive a rebuild, and never wipe on a failed read

Reset drops every derived table, doc rows included, and the doc pass only fired on a tip change or once at boot — so a reindex left documentation silently unsearchable until somebody happened to commit, which on a mirror of a quiet project is never. Observed live: fossil's 173 documents went to zero after a rebuild and stayed there.

ReplaceDocs swaps the whole set, so a pass that found documents and could not read them would overwrite a good index with nothing. It now keeps the previous set and says so. A tree with genuinely no documents still writes an empty set, which is true. closed check-in: 24586a4771 user: dmestas tags: doc-reindex

20:26
Praise UI: a button on the repo, a listing to reverse it

Restores the backend the trunk merge dropped, and adds the two surfaces that get pressed.

The button sits in the stats strip beside the clone count, drawn only when there is an account behind it. Pressed state rides on aria-pressed as well as colour.

/praised is where praise is revisited and withdrawn. It exists apart from /people/{user} because that page lists CONTRIBUTORS derived from the DAG, and a praiser need never have authored anything. check-in: 0cba46d4ed user: dmestas tags: praise-ui

20:21
merge doc-search into trunk check-in: 547934f37f user: dmestas tags: trunk
20:20
search: documentation is searchable (fossil's scope "d")

quarry rendered tree documents as a first-class surface — the docs tab, the reading furniture — and could not find them. A reader could open www/quickstart.wiki and get nothing searching for it. Fossil searches c d t w e f; quarry had everything but d.

Indexed at the default branch tip only, asynchronously, on the trigger warmReads already uses: a round that moved the tip, plus once at boot. Not through dagnats — dagnats is optional, a forge without one "simply has no CI rather than a broken one", and search must not inherit that. This is local derivation from the store, the same class of work as the fold, which Reindex already routes around the bus on purpose.

isDocPath is exported so the indexer and the docs listing share one definition of what a document is; two would drift and the drift would be invisible.

Adding the kind exposed how many places the list is written down: the index, two ops validators, the scope chips, hitOf. The validator was missed and refused every doc search before it reached the query. Two tests now hold the four in agreement. closed check-in: 57a2f8e3d2 user: dmestas tags: doc-search

20:18
Merge trunk into praise closed check-in: 7b32366269 user: dmestas tags: praise
20:17
Suite green; merges clean after clone-counts landed. check-in: d4add44f21 user: dmestas tags: trunk
20:16
Suite green; merges clean. Proven against a real server: two clones read 2, a pull leaves it at 2. check-in: bc4fbd1c79 user: dmestas tags: trunk
19:59
Praise: vocabulary, storage, ops

Any signed-in account may praise any repository it can SEE — no capability letter, because the read gate has already decided whether the repository exists for this caller and past that gate praising is not a privilege anyone needs granting.

Per account, not per contributor: someone who has never authored an artifact can still say a repository is worth attention, which is why this cannot hang off the contributor pages.

praise.list filters by the VIEWER, never the owner. Praise is public; the existence of a private repository is not, and this is where the two meet.

UI deferred: the button belongs in the repo stats strip, which is on the unlanded clone-counts branch. check-in: f1a6be9a77 user: dmestas tags: praise

19:59
merge search-links into trunk check-in: 11c9c4a43b user: dmestas tags: trunk
19:47
Settings fits a phone

The capabilities table sized to an <input size=28> it could not shrink, so the whole page was wider than the viewport and every field ran off the right edge. It scrolls in its own box now.

Webhooks were a three-column table with an input and three buttons in one cell — the same bug, added by me. Rows stack instead.

Help text cut to one line; the noun chips and the field's datalist say the rest. closed check-in: 0a60aac30c user: dmestas tags: settings-mobile

19:16
RFC 0005: state what the clone counters count closed check-in: c83148c71d user: dmestas tags: clone-counts
19:16
Count clones and fetches

A clone reads and writes nothing, so it is the one kind of forge activity that leaves no trace in the DAG at all.

The sync edge classifies the protocol's cards: clone SEQNO 1 opens a new clone, later rounds do not, so a big store cloned in forty rounds still counts once. Two sinks: the vocabulary event for anything reacting, and a JetStream counter for how many, which converges across nodes instead of losing increments to a read-modify-write. check-in: dc058fa551 user: dmestas tags: clone-counts

18:08
Suite green across 29 packages; merges clean.

Four commits: the fan-out onto the vocabulary, per-subscription event selection, the settings surface, and the RFC correction that preceded them. check-in: 69fc3f06d1 user: dmestas tags: trunk

17:20
Webhook settings on the repository's own admin screen

Subscriptions were CLI-only. The settings page now lists them, takes an endpoint plus its event patterns, pauses, resumes and deletes — gated on 'a' like everything else there. Patterns are checked before saving.

The section is gated on the store being attached, not on the list being non-empty, or the first subscription could never be created. closed check-in: 1c5d758256 user: dmestas tags: hooks-vocabulary

17:16
Subscriptions select their events

A webhook took everything or nothing. It now names vocabulary patterns: '*', a bare noun for every action on it, or an exact kind. Checked at the moment it is set, because a webhook that silently delivers nothing reads as a broken forge. check-in: e9547dcc49 user: dmestas tags: hooks-vocabulary

17:13
Webhooks fan out from the vocabulary, not from the ops layer

A ticket closed by push notified nobody, because only ops queued webhooks. One consumer on quarry.evt.> turns every raised event into deliveries, so arrival path stops mattering.

Deletes kindOf: the delivery's kind was sliced off an idempotency key. check-in: f2ea5f5c1a user: dmestas tags: hooks-vocabulary

17:03
@dmestas I love the strata idea check-in: edcc983217 user: dmestas tags: trunk
16:57
Close the push gap: derived events all come from the fold

The index holds the state an op would have read, so a pushed artifact gets the same event a forge op would have raised. Ticket transitions and landing verdicts compare folded state before and after; forum, wiki and technote need only the artifact or one lookup.

Op sites stop raising derived events, so 2.1's split is now enforced rather than documented. check-in: 06942dcc5d user: dmestas tags: trunk

16:37
RFC 0006: strata — stacked landings, rendered as a chain

The forge half of zit#144. A stack is a chain of dependent landings that land bottom-up; quarry renders the chain, refuses accept above the bottom, and flags a segment whose base was rejected.

No new index and no new artifact: the edge is already implied by a landing's target, so a stack is a reading of landings the way a landing is a reading of forum posts. closed check-in: 041ca46258 user: dmestas tags: strata-rfc

16:32
RFC 0005: record what implementing the derived events taught check-in: 17317e830f user: dmestas tags: trunk
16:32
Raise the derived events from the fold

checkin.created, branch.created and tag.* come off the artifact; branch.moved comes from the computed branch after RecomputeBranches, because only a branch's first check-in carries a sym- tag.

Raising is not idempotent while folding is, so the fold asks the index which hashes are new before writing them. A rebuild raises nothing. check-in: a1b816cd96 user: dmestas tags: trunk

16:27
Add the new event-plane files the previous commit omitted check-in: f485551646 user: dmestas tags: trunk
16:27
Publish the vocabulary on quarry.evt.<repo>.<noun>.<action>

RFC 0005 picked zit.evt., which the fold consumer already claims with a trailing wildcard — every vocabulary event would have been handed to the artifact parser. Own stream, own root, tests pinning both. check-in: 3bca39d8f5 user: dmestas tags: trunk

15:57
RFC 0005: dagnats already has the triggers; correct the record

I probed HTTP and concluded dagnats had no scheduler. Wrong lens — triggers are NATS and KV native. It has cron, subject, webhook, durable HTTP, worker-contributed kinds and debounce. So quarry builds none of that: it owes well-named subjects, and the vocabulary IS the hierarchy. check-in: 3aef7f3211 user: dmestas tags: trunk

15:50
events: the vocabulary as code, and what the bus really does

RFC 0005 §1 as internal/evt, named at the eight sites that raise events instead of sliced out of a TxnID — so a ticket change that closes one is ticket.closed, not ticket. Measured the window: a gap IS detectable (first_seq - ack_floor) but the next fetch succeeds silently, so every consumer must check. dagnats has no scheduler; quarry owns the clock. check-in: f08add75f5 user: dmestas tags: trunk

15:36
RFC 0005: verified the bus, added schedules and dispatch

The stream is a one-hour window, so events split: DAG-derived ones are re-derivable after a gap, operational ones need OLTP backing. A cron run and an ad-hoc run are not events — three trigger sources, and schedules take fossil's 'a' so a repo admin owns their own. check-in: 60206e85b2 user: dmestas tags: trunk

13:11
RFC 0005: one named event stream, three consumers

Events are noun.action, subscribed by name. Fossil has no such vocabulary — its hooks are artifact-level and its alerts are eight letters — so the delivery semantics come from its warning that a distributed system cannot promise exactly-once. check-in: dc2bb819a3 user: dmestas tags: trunk

12:28
merge timing-wall into trunk check-in: 952100a76b user: dmestas tags: trunk
12:27
timing: the footer reports wall time, not a sum of concurrent calls

A diff is two zit subprocesses on purpose, run at once. Adding their durations printed more time than the whole request took — "rendered in 410.1ms (zit 706.8ms×2)", and "79.94s (zit 159.05s×2)" on a cold tcl page. A number larger than the total reads as a broken page rather than as two calls overlapping.

Record now takes the start instant instead of a duration, because a duration cannot say whether two calls overlapped, and the union of the spans is what a reader actually waited. closed check-in: 4aaf089616 user: dmestas tags: timing-wall

10:47
merge people-admin into trunk check-in: f8af01dec3 user: dmestas tags: trunk
10:47
people: an admin sees which contributors are also accounts

A contributor is a name an author's client wrote on an artifact; an account is a login on this forge. On a mirror the two barely intersect — fossil has 151 contributors and this forge has two accounts, sharing none — so the page says so, and badges the few that are both.

Read-only on purpose. A grant control here would offer to give write access to a string, and on a mirror those strings are chosen by whoever can commit upstream. The door goes to settings, where every row is a real account.

account.list is forge-wide and site-admin only, so a repo admin gets the door without the badges rather than an error. closed check-in: 8fb0a669aa user: dmestas tags: people-admin

10:25
merge repo-people into trunk check-in: 5757eeaaeb user: dmestas tags: trunk
10:25
people: a repository's contributors, inside the repository

"151 contributors" in fossil's About panel went to the fleet page: 481 names across 25 repositories, repo nav gone, search rescoped to the whole forge. people.list has taken a repo parameter all along and nothing in the UI reached it.

The per-person line differs from the fleet's on purpose. Counting repositories inside one repository always says 1, so the span of somebody's work here is the fact worth carrying instead.

Contributors(0) meant a hundred, silently, which is why the listing said "of 100" under an About panel saying 151. It means all of them now. closed check-in: 278e021086 user: dmestas tags: repo-people

09:52
merge landing mimetype with trunk check-in: cfd042dd99 user: dmestas tags: trunk
09:52
landings: say the mimetype, and keep headings to one line

zit#136 shipped --mimetype, so a remark written in the composer is stored as the Markdown it previewed as. zit#138: the title card holds the whole -m message, so the heading takes its first line. check-in: 73367a3820 user: dmestas tags: trunk

09:51
about: the notes and announcement rows join the panel's icon grammar

Every other row in the About panel is icon + label, and .about-sub indents 1.5rem to clear that icon. These two had no icon, so their excerpt and date hung indented under nothing. A wrapping title also needs baseline alignment, or its later lines slide back under the icon.

The note now says who wrote it, as the feed does. check-in: d6a6146502 user: dmestas tags: trunk

09:46
merge notes-first into trunk check-in: 116d10e5cb user: dmestas tags: trunk
09:46
about: a project's news comes before its version list

Notes sat third in the sidebar, under Tags. What a project has been doing is a better first answer to "what is this" than which versions it has cut — and with no tab, this is a main door to them, which reads poorly third. closed check-in: d1dde173e0 user: dmestas tags: notes-first

09:41
landing page: probe the tip pair instead of scanning for the record

zit#134: landing show has to walk the whole store to FIND the record before it can answer about it, and quarry already folded that record out of its own index. zit merge --probe names the two check-ins, so the walk disappears -- 0.005s against 0.18s on this forge's own store, and the gap widens with store size, not with the number of landings.

Probing the same tips the cache is keyed on also closes a race: the key was built from resolved tips while the probe re-resolved a branch name, so a push landing between the two cached an answer under a key it did not describe.

An unresolvable tip now skips the probe instead of keying on ':' -- every landing whose branch could not be resolved shared that one cache entry and could be served another landing's verdict. check-in: bb05986ad8 user: dmestas tags: trunk

02:57
notes: the notes-filtered timeline is a door, not a dead end

Without a tab, /timeline?y=e is where somebody browsing notes stands, and it offered no way to the page that reads them as a feed. The other two doors — the About panel and the docs page — are not where a reader who just filtered a timeline is looking. check-in: 16e8738cb6 user: dmestas tags: trunk

02:40
merge forum/landing separation with trunk check-in: f0f4f7c525 user: dmestas tags: trunk
02:39
forum: landings are not forum content

A landing is a forum thread — that is what a stock fossil clone reads — but it is a decision record with its own surface. Off the listing and out of the count, which must agree. An existing thread URL redirects to the record rather than 404ing a link that used to work. check-in: 302ca7fae6 user: dmestas tags: trunk

02:39
merge notes into trunk check-in: 1ff2886793 user: dmestas tags: trunk
02:38
notes: one row per technote, not one per edit

Fossil registers one timeline event per technote; quarry registered one per artifact. libfossil has 50 technote artifacts under 26 event ids, so its timeline showed a superseded title five rows above its own correction. The collapse is in the event trigger — insert, then evict every loser for the event id — which is order-independent, as the fold requires: a clone delivers artifacts in wire order.

Notes are no longer a tab. Fossil's built-in menu has none, and libfossil, which uses them as a milestone feed, overrode mainmenu to add TODOs and FIXMEs without adding one. They are reached from the About panel, the docs page and the timeline filter instead, and the listing is a feed with excerpts rather than a list of links.

Every edit stays readable, as a wiki page's revisions are.

Fixes an authoring bug found on the way: zitcli never passed --time, so an edit re-filed the note at the moment it was edited and it moved in the timeline. The filing time is now a field on both forms. closed check-in: ac80214995 user: dmestas tags: notes

02:34
spectrum: separate colliding voices by lightness, never by hue

Two names whose hues land 7 degrees apart painted one block. Near collisions are a birthday bound, not bad luck, so the bar resolves what is in the thread — stepping a colliding voice to the identicon's other lightness. The hue never moves: it is the identity. check-in: e1e9619d0d user: dmestas tags: trunk

00:06
pages must be revalidated, not guessed at

An HTML response carried no cache directive and no validator, which does not mean do not cache — it lets the browser choose a freshness lifetime. Safari chose one, and served a timeline rendered before a deploy while the server answered with another; the reader sees stale state with no way to tell.

no-cache rather than no-store: the copy is still kept and reused after revalidation, so the back button stays instant. Only reuse without asking is wrong, and every one of these pages is a view of moving state. check-in: ece3b20f3d user: dmestas tags: trunk

2026-08-23
23:43
docs list: a byline inside a link stays plain

An anchor inside an anchor is invalid — the parser closes the outer one at the inner, which tore every docs row in half. The docs list and the wiki home card are whole-card links, so their bylines go back to plain mark-and-name; the row already goes somewhere.

a.who is inline rather than inline-flex for the same family of reason: a flex box is atomic and cannot flow with the text around it. check-in: 01e7bce146 user: dmestas tags: trunk

23:34
state the page's own address, so enhancement works behind basic auth

A URL with credentials (http://user:pw@host/) leaves them on document.baseURI even though location is cleaned, and fetch() refuses any URL carrying credentials — so every enhancement failed at once: diff toggle, unread poll, diff sentinel. A server-stated <base> fixes all of them without script, since the server knows the URL from its own routing and never from userinfo.

The toggle also swallowed its own fallback: replaceState to a credential-free URL throws on such a page, and it ran before @get while __prevent had already cancelled the link — neither swap nor navigation. Cosmetics now cannot fail the click. check-in: c35d8c4b5b user: dmestas tags: trunk

23:14
a person is a link: identicon and name go to their timeline

Fossil routes every surface through one helper (hyperlink_to_user); quarry had thirty-two places rendering a name and none of them clickable. One 'who' helper now, applied to the twenty that name an author — the account button and participant stacks stay decorative.

timeline.list gains user=, paired with y=all: what someone did includes their wiki edits and ticket changes, not just check-ins. check-in: a18be6d78d user: dmestas tags: trunk

23:08
timeline: internal nodes match the leaf's painted size

The ring is stroked 2.2, so it paints to r+1.1; a 4.8 disc beside it read a fifth smaller. Internals are the ring's outer edge, filled. check-in: e1f3ed4bac user: dmestas tags: trunk

22:37
markdown: a mention reads like a byline

Mark then name, in the page's colour, orange on hover. The sigil is syntax and the mark already says this is a person; a name that is orange everywhere stops meaning clickable anywhere. check-in: dda6810776 user: dmestas tags: trunk

22:31
markdown: mentions carry the identity mark, quotes are visible

An @mention now renders the same glyph as the person's byline, through the resolver seam so markup still knows nothing about accounts. The identicon moved to internal/idn — web and api each hashed the name separately and agreed by luck. Blockquotes had no rule at all. check-in: 0ba22dea9e user: dmestas tags: trunk

22:19
landings: the forum's post card, and the tracker's list

f-post-h and f-post-b are where the padding lives; naming the wrapper alone gets a border round unpadded text. Check-ins use the same bordered list every other listing in the forge uses. check-in: 20373a1806 user: dmestas tags: trunk

22:13
landings: use the shared composer frame, collapse the diff

The composer's textarea has no border of its own — it is built for the f-reply wrapper the ticket page and forum use, and without it the tabs and text floated unframed. The diff is a details now: this page is where a decision is made, and 600 lines between the proposal and the buttons means scrolling past the change to reach them. check-in: 59d7d8fa4c user: dmestas tags: trunk

22:04
merge trunk: landings

Textually clean, but branchTipOf called Timeline with the old positional signature — ported to TimelineFilter. check-in: dae2180a89 user: dmestas tags: trunk

22:03
check-in page: tag history, branch diff, manifest view

Fossil lets an author correct a check-in comment after the fact; quarry showed the superseded C card as if it were current, on 697 fossil check-ins. The correction is now applied at read, batched in AttachCI, with the original kept beside it — the stored card is an artifact fact and stays untouched. hidden and closed surface too.

Branch diff is the branch against its fork point, which is not the landing page's base (the target's current tip). Offered only where a fork point exists.

The manifest view renders the eight cards that are not the file list and counts the other 980, linking the tree.

Also: unified/side-by-side now swaps in place via datastar, keeping the window and correcting the URL; the wiki note panel lets blocks use the full width while prose keeps its measure. check-in: ff7f5d3d69 user: dmestas tags: trunk

22:01
landings: a way to open one, and the lifecycle written down

docs/landings.md covers the whole loop and says plainly that a landing is a forum thread plus branch tags. Writing it found the gap: there was no create control at all. A mirror offers its upstream instead. check-in: c6aa693938 user: dmestas tags: trunk

21:50
landings: prove the lifecycle end to end

open, comment, reject, accept through the forge against a real store — accept merges to trunk via zit#131's bare-store path. Also checks the record still reads as an ordinary forum thread. check-in: 9a84a5d962 user: dmestas tags: trunk

21:43
landings: memoise the mergeability probe on the tip pair

The probe is a full-store scan by design and measures 2.0s here, paid on every view. Keyed on the two check-ins, so a push invalidates it by making a different key rather than by anyone clearing a cache. 1.9s cold, 50ms warm. check-in: 21d510a5bd user: dmestas tags: trunk

21:38
landings: list the branch's check-ins

A proposal is changes with reasons attached; a diff keeps the changes and throws the reasons away. zit#133's display half, from our own index. check-in: 120277efc7 user: dmestas tags: trunk

21:35
merge landing mergeability with trunk check-in: 537889b661 user: dmestas tags: trunk
21:35
landings: read mergeability from the store, never from an attempt

zit#131 landed the probe and exit 5. A conflicted landing renders as blocked with its paths named and accept disabled; a refused accept is a state to show, not a fault to report. Unknown is never treated as clean. check-in: e0f59b6abf user: dmestas tags: trunk

21:06
check-in page: patch link, full hash + copy, tags, change stats

Stats are tallied at parse time so a windowed diff reports the whole file, and sit on the file header as well as the summary line. Tags come from checkin_tag, newest word per name winning. Copy falls back through execCommand to selection: navigator.clipboard is undefined on the plain-HTTP origins quarry is usually served from. check-in: ac056aaa1d user: dmestas tags: trunk