Skip to content
quarry Timeline
Login

Timeline

50 descendants and 50 ancestors of 76b09e7fb83efa59

2026-08-17
20:51
web: report where a request's time went, not just how long

The footer said 'rendered in 344ms'; it now says 'rendered in 344ms (zit 270ms)'. One number says a page is slow, not which layer to look at, and quarry's layers differ by three orders of magnitude — an index read is microseconds, zit ls --long on tk is 2.6 seconds.

The accumulator rides the request context, which is what makes it correct under load: package-level counters would blend concurrent requests together, and concurrency is what this exists to measure.

zit read calls take a context now and use exec.CommandContext, so an abandoned request kills its subprocess instead of leaving it to finish a 2.6-second listing nobody will read.

SQL is not instrumented yet; the index does not take a context. check-in: 85b9909d1c user: dmestas tags: trunk

20:42
index: denormalize the timeline into an event table

The timeline assembled itself at read time from a five-way UNION ALL, ending in USE TEMP B-TREE FOR ORDER BY — every request sorted every event in the repository to return fifty. tk cost 536ms; fossil serves the same page in 3-4ms whatever the repository size.

This is fossil's shape: one event table with a kind column and an index on date, so the read is an index walk that stops at the limit. Triggers maintain it, like search_fts, so it cannot drift from its sources. isleaf and the ticket label are resolved for the returned page only.

tk 536ms -> 4ms sqlite 210ms -> 5ms fossil 163ms -> 3ms pikchr 22ms -> 1ms home 399ms -> 3ms (it asked each repo for its newest event)

plan_test asserts the plan, not the types: nothing about the old query was ill-typed, and the defect was only ever visible in EXPLAIN.

Also fixes limit being ignored on every wildcard route: the router's second decode pass re-applied defaults to fields the path did not name, so ?limit=200 became 50. OverlayValues sets only what it names. check-in: 450a8ad0f3 user: dmestas tags: trunk

20:32
web: report render time in the footer

Fossil publishes its own ('This page was generated in about 0.003s') and that is what made our timeline regression measurable against it. Same number, same place, every page.

semdiff gains data-volatile so the disposability harness compares state rather than the stopwatch — without it a wipe-and-reingest fails on '<1ms' vs '4ms', which is true about the clock and false about the index. check-in: cb17edbefa user: dmestas tags: trunk

19:11
web: compact the file listing

Was ~52px per row across three columns for one short filename; fits 38 entries where 25 fit before. Drops the History column — thirty identical links, and the file page already links its own history — adds dir/file marks, right-aligns sizes with tabular figures, and folds four lines of chrome into one.

Names are no longer underlined: every name in the listing is a link, so marking them all marked nothing.

The space the History column freed is where the last-change column goes once the index carries per-file data. check-in: 574cf5b97e user: dmestas tags: trunk

18:57
api: pick README markup by extension, not by assumption

A bare README was rendered as markdown 'by convention'. libspatialite's is an autotools plain-text file whose commands carry the root prompt #, so markdown read '# sudo make install' as a heading and turned the build instructions into a dozen h2s.

Extension decides now, and anything unrecognized falls through to escaped

 with its line breaks intact — fossil's rule in
wiki_render_by_mimetype. Also recognizes README.wiki.
check-in: 1499d5aa5a user: dmestas tags: trunk
18:49
web: repository nav shows counts and current section

Counts tell a reader where a store's content is — pikchr is 245 forum threads and one ticket, tk is 5,809 tickets and one post. The index memoizes them per fold; on tk they cost ~110ms cold and 3ms after.

Also: the current section is marked, empty sections are dimmed but stay linked, the repository outranks the forge name in the header, and timestamps render relative with the exact value in the title. check-in: 3a330c9560 user: dmestas tags: trunk

18:03
docs: fleet admin and mirroring in running.md check-in: e79c732ee4 user: dmestas tags: trunk
18:02
fleet: mirror upstreams on a schedule

Adds mirror.list/set/delete/run ops (admin), a mirror table in OLTP, and a scheduler that pulls each configured store on its own interval. Pulls run under the store's single-writer lease and re-ingest only when artifacts actually arrived.

zitcli.Pull is the one call that cannot pass -R: zit pull refuses the flag and works by working directory. pullChanged reads zit's result positively and errors on an unrecognized line rather than defaulting to 'nothing changed', which would leave a mirror green while it silently stopped tracking. check-in: 9c7b4cff5b user: dmestas tags: trunk

17:50
fleet: per-store ingest health and manual sync

Adds fleet.status and fleet.sync ops (admin) and a /fleet page: last round, artifacts, parse fallbacks, and consecutive failures per store. With eleven repos a wedged store is invisible on a page that shows only counts — its counts are whatever they were when it broke.

gate gains gateAt so the fleet pages can ask for Admin at an empty repo rather than Token at a repo; forbid now words the refusal to match. check-in: 5c9f9f41b1 user: dmestas tags: trunk

17:18
ingest: stream the census instead of buffering it

FramesEach hands each artifact to a callback as it is read; peak RSS on sqlite's 13.2GB census drops to 0.48GB. Frames is kept as a collecting wrapper for tests. A callback error kills the child rather than draining the stream, and the cursor is not saved on a failed round. check-in: 05d01cbb93 user: dmestas tags: trunk

16:58
stop tracking agent tooling's files

The harness writes .agent-config/trace/<session>.jsonl into the working tree and it was tracked: 2MB, carried across ten check-ins, growing with every tool call, so each commit stored another revision of a log nobody working on quarry writes or reads. .claude/scheduled_tasks.lock is a lock file and equally not source.

Both untracked with 'zit rm', which keeps the files on disk, and already ignored via .zitignore so they do not return as extras. History keeps what was recorded; this stops the accumulation. check-in: 4d8f962778 user: dmestas tags: trunk

16:58
stop tracking agent tooling's files

The harness writes .agent-config/trace/<session>.jsonl into the working tree and it is tracked: 2MB, carried across ten check-ins, growing with every tool call, so each commit stored another revision of a log nobody working on quarry writes or reads. .claude/scheduled_tasks.lock is a lock file and equally not source.

Both untracked with 'zit rm' (the files stay on disk — zit keeps them unless asked otherwise) and added to .zitignore so they do not come back as extras. History keeps the revisions already recorded; this only stops the accumulation.

Also lands gofmt churn in web_test.go from an earlier formatting pass: import order and one alignment block, no logic. check-in: bcd8d75db7 user: dmestas tags: trunk

16:53
deploy: the image builds and serves — three faults only running it found

The Dockerfile and compose were committed unverified because no container runtime was installed. With one available they were wrong in three ways, none visible by reading:

- The Zig download 404s. Naming is zig-<arch>-linux-<version>; I had the older zig-linux-<arch>-<version>. - The zit clone fails: the GitHub mirror is private, so an anonymous clone asks for a username. zit's source now arrives as a named build context, which needs no credentials and pins the exact checkout being shipped. A public fossil mirror exists at fossil.craftdesign.group/zit and would make a clone step viable later. - /data was root-owned. VOLUME creates a missing directory as root while the process runs as uid 10001, so quarry could not open its own database — 'permission denied' on the first line of the log.

Now verified end to end: a 90.1MB image, /etc/quarry-build recording both versions, and a container serving libfossil's 3,835 check-ins from a read-only mounted store with zit reading it from inside. check-in: a8cbc48878 user: dmestas tags: trunk

16:48
ignore generated fixture stores

fixtures/out is 599MB of built stores and it is what a 'zit addremove' once swept into this repository, along with a 45MB binary — 559MB of bloat that took a shun and a reclaim to undo.

Ignoring the BUILT output rather than the whole directory: the scripts and README beside it are source.

.zitignore rather than the ignore-glob setting, because zit says plainly that a root .zitignore overrides the setting, and because a tracked file travels with a clone while a local setting does not. check-in: 52a8ca8b77 user: dmestas tags: trunk

16:40
deploy: Dockerfile, compose stack and a run book

quarry had no packaging at all while notify already had a working Dockerfile and compose. The image ships zit WITH quarry, deliberately: 0001 §2 makes the zit CLI the entire store interface, so zit's behaviour IS quarry's, and today alone three zit builds changed what quarry does — frames got faster, a card fix changed which artifacts parse, stat -R enabled bare-store serving. ZIT_REF is a build argument and /etc/quarry-build records the pair, so a container can answer which zit it carries.

compose follows notify's shape, including the constraint its own file documents: dagnats binds loopback in standalone mode, so everything shares its network namespace. quarry joins it too — its embedded NATS uses DontListen and binds nothing, so there is no port to collide with.

docs/running.md is the by-hand path, which is what this was actually developed against: the launchd-signed dagnats, notify's bootstrap and workflows, the topic-vs-subscriber addressing, and the two credentials that close the write and push gates.

Unverified, and said so in the doc: no container runtime is installed here, so the images have never been built. The two build recipes inside them ARE verified natively — zit compiles from a clean clone with zig build --release=fast, quarry builds fully static with CGO_ENABLED=0.

Also lands the publish-throughput measurement as an opt-in test. It is what disproved the hypothesis that synchronous publish was the ingest bottleneck (62k msg/s sync), which redirected the search to zit frames and quarry's folding. check-in: bbe731e042 user: dmestas tags: trunk

16:35
archives: download a check-in as a tarball (blocked on zit#97)

zit#95 landed 'zit tarball', so quarry can offer what every forge does: this check-in as a file. An op, a download route with the same posture as attachments (always a download, never an inline render), and a link beside the clone box.

It does not work yet, and the reason is upstream: zit tarball ignores -R and demands a checkout in the working directory, which a server process never has (zit#97). ls honours -R from the same directory against the same store; tarball does not. The route is correct and stays — it starts answering the moment the flag is honoured — and the failure is reported as unavailable rather than not-found, because the repository is there and the archive is what cannot be produced.

Not worked around: 0001 §2 makes the CLI the whole store interface, and a checkout materialised per download to dodge the bug would defeat both that and the reproducibility the feature exists for. check-in: c81b757987 user: dmestas tags: trunk

15:52
web: the files page was unusably slow; fix the query, memoise, cap the list

Three separate faults, worst first.

Branches() used a correlated subquery — one full scan of checkin per row. On tcl (42,074 check-ins, 1,855 branches) that took 434 SECONDS, which is what made the page look hung rather than slow. One grouped scan with SQLite's bare-column-with-max rule, plus an index on (branch, date): 0.042s.

'ls --long' costs ~4ms per file because it reads each blob for size and the binary flag, so tcl's 2,198-path tip took 8.5s. Fossil never pays this — browse.c builds a directory listing from its own indexed tables and shows no size at all — but quarry reads through the CLI by design, so the answer is to compute once: a check-in's file set is IMMUTABLE, so listings are memoised per version, the tip is warmed after each ingest, and the tree op resolves 'trunk' to its hash so the page asks for the key that was warmed.

Only a hash may be cached. Caching 'trunk' would serve a stale tree forever after the next commit — a correctness bug dressed as a performance fix — so the memo refuses anything that can move, with a test pinning it.

The branch selector rendered every branch: 1,855 <option> elements, 90% of a 239KB page, and unusable as a control. Now the 25 most recently active, plus the branch being viewed even when it is older.

tcl's repo home: 8.5s -> 0.38s, 239KB -> 29KB. sqlite: 5.0s -> 0.32s. check-in: c754084dc7 user: dmestas tags: trunk

15:12
artifact: an empty U card reads as anonymous (§4.7.17)

A present-but-empty U card — a bare U, or one followed by an empty token — is specified as the literal string "anonymous", and quarry left the author empty. The two are different claims: one says an anonymous person did this, the other says nobody did.

Absence of the card entirely stays a separate matter, governed by the per-type required-card rule of §5.2.

Noticed while tracing the §4.2 empty-token defect (zit#86), which is the same shape: a MUST a reader can skip without anything complaining. check-in: adf5f7c322 user: dmestas tags: trunk

14:47
ci: verdicts on the timeline and check-in, live run status

Two follow-ups to the CI slice.

A check-in now carries its verdict wherever it appears — a pass/fail badge on timeline cards and the check-in page, and a ci field on the API. It is read from the store's tag artifacts, not from dagnats, so it answers with the engine down and travels with a clone. A check-in with no verdict shows nothing rather than a neutral badge that would read as a result.

The CI page follows runs live. dagnats' /runs/{id}/events is a JSON snapshot rather than a stream, so this polls and sends only when the rendered fragment actually differs, reusing the SSE shape the timeline and fleet already use. data-init sits on a wrapper OUTSIDE the patched element — the mistake that silently killed both other streams this morning and only showed up in a browser.

Verified in a browser rather than by asserting the server is right: a commit to synthetic made the table go from one row to two with no reload. check-in: f50a282ab1 user: dmestas tags: trunk

13:13
ci: wire the trigger, poll runs, record the verdict as a tag

Completes the loop 0001 §6 describes. The trigger runs after each ingest fold — the moment a check-in becomes visible whatever brought it in, a local commit, a push or a sync. A poller refreshes runs quarry still thinks are going, and a terminal run becomes a tag artifact on the check-in, so CI history clones with the repository instead of living in the forge.

The verdict tag is --raw, and that is not a detail: without it the name enters the symbolic namespace where it resolves to a check-in like a release name, and the SECOND check-in to pass would make 'ci-pass' ambiguous for both. zit#80 landed exactly this last night.

It is written through the lease holder, like every artifact quarry mints, so two nodes cannot both record a verdict.

Proven end to end against the live dagnats: a commit to synthetic launched ci/build.json read AT that check-in, the run was recorded, cancelling it drove the poller to write ci-fail with the run id as the tag's value, and the tag came back through ingest into the index. check-in: e326831901 user: dmestas tags: trunk

13:02
ci: the trigger half of v3 — dagnats is the engine

0001 §6 splits this narrowly: dagnats runs pipelines, quarry notices a check-in and shows the result. quarry defines nothing, schedules nothing, retries nothing.

A pipeline is TRACKED CONTENT — ci/*.json in the repository, dagnats' native format — read at the check-in under test, so the pipeline for a historical check-in is the pipeline that ran and there is no forge-side store to drift. Only files directly under ci/ count; a nested path is somebody's fixture.

Two behaviours worth naming. First sight of a repository takes the tip as a watermark and launches NOTHING: running a pipeline per historical check-in would be a denial of service against the engine and would say nothing about code nobody just changed — on an imported corpus that is tens of thousands of runs. And an unrecognised run status counts as still running, because claiming a run finished when it has not is how a verdict gets written against a build still going.

Verified against the live dagnats for the wire shapes; the trigger itself is covered by tests. check-in: 0502a93834 user: dmestas tags: trunk

01:12
web: a repository has a front door — file browser and README

/r/{repo} was a 404. A repository's landing page was the timeline, which is fossil's instinct rather than a forge's, and there was no way to browse files at all: /file/{hash}/{path} served content only if you already knew the exact path, and nothing linked to it.

0002 §1 lists this as a v1 row that never got built. tree.list turns zit ls's flat path set into one directory level — a store has no directory objects, so a directory is a shared prefix and a count of what is under it — with the directory's README rendered beneath, breadcrumbs, and files linking into the existing highlighted view.

The branch selector carries each tip hash, not just the name: zit permits two branches to share a name where forgejo assumes uniqueness, so a name-keyed control would silently pick one.

The clone box emits the reader's own host, since a hardcoded base is wrong for every deployment but one. check-in: 7058721205 user: dmestas tags: trunk

00:57
tickets: display them the way fossil models them

The page dumped every folded field into one table, which read as noise and lost most of the ticket. Fossil's model has three parts and quarry was collapsing all of them into one:

- Scalar state (status, priority, resolution…) folds last-write-wins. - A leading + is the APPEND form: 'J +comment more' grows the field rather than replacing it. quarry stored a separate field literally named '+comment' — sqlite's tickets use it 255 times. - icomment belongs to the CHANGE, never to the ticket. Folding it left one comment standing where a conversation was: the busiest tcl ticket showed 1 of its 84.

Per-change plumbing (icomment, mimetype, login, username) no longer appears as ticket state. The page now leads with the title, shows fossil's header order, renders the description and every comment through their own recorded mimetype, and attributes each — including fossil's 'claiming to be' when the typed name differs from the login, which on an open tracker it may.

Verified on tcl 408840ff: 11 clean fields, 84 comments, 5 attachments. check-in: 9ac012f7f2 user: dmestas tags: trunk

00:32
notify: address repo activity as a topic, with a key notify accepts

Running the real companion found two faults no stub would have.

NotifyEvent.To is documented as "a user, or a repo:<name> topic", but quarry put that string in subscriberId. notify addresses the two through DIFFERENT fields, so an activity event did not fan out — it invented a subscriber literally named repo:synthetic and every watcher heard nothing. Verified against notify: the message existed, addressed to nobody real.

notify also refuses a topic key containing a colon (lowercase, digits, hyphen, underscore only) and rejects the whole trigger, so quarry's own convention could never have been a valid key. repoTopicKey translates at the boundary; quarry keeps its internal vocabulary. Names differing only in punctuation or case collapse to one key, which is documented rather than hashed away — an operator has to be able to read the key they subscribe someone to.

End to end on a real notify + dagnats: a wiki save reaches every member of repo-synthetic, an @mention reaches the person, and both render in quarry's inbox. check-in: 526e25a5d6 user: dmestas tags: trunk

00:19
notify: authenticate triggers, and embed the inbox

Two things, one of them a live bug. notify's /v1 routes sit behind an API-key middleware and quarry's client sent no Authorization header, so every trigger it fired would have been answered 401. It went unnoticed because --notify was never configured and a trigger failure is logged rather than raised. --notify-key (or QUARRY_NOTIFY_KEY) supplies it, and serve says so at startup when it is missing rather than leaving the answer to be discovered in the log.

The inbox is notify's, embedded (0001 §5): GET /v1/messages?channel=in_app for the signed-in account, rendered with unread emphasised. quarry models no notification state — it displays what notify owns.

The wire field names are notify's Go field names: its domain.Message carries db tags but no json tags, so the encoder falls back to the identifiers. A test pins that, since decoding lowercase would silently produce empty subjects.

An outage is not emptiness: a failed fetch says notifications are unavailable instead of showing an empty list, which would be a lie about whether anyone had written to you. check-in: e33f1c4e8c user: dmestas tags: trunk

00:10
web: cookie sessions and a login page

Basic auth works and stays for the API, but a browser has no way to log out of it and the credential lives in the browser's keeping. A gated page now redirects to /login instead of raising a Basic prompt, the header names who is signed in and offers sign-out, and Basic remains for scripts.

Session tokens are stored as SHA-256 digests, so the table cannot be read back into working cookies. Cookies are HttpOnly, Path=/, SameSite=Lax — Lax being what stops another origin POSTing quarry's write forms on a reader's behalf — and Secure under TLS.

A password change or an account deletion ends that account's sessions; otherwise 'change your password' would leave the browser already holding one still signed in. Authorization for a cookie runs the same RoleOf lookup the password path uses, so a cookie is never worth more than a password.

safeNext keeps the post-login redirect on this site: a reader is at their most trusting on the hop straight after authenticating, which is exactly when an open redirect pays. check-in: 7bdf7a189b user: dmestas tags: trunk

2026-08-16
23:55
index: pin the artifacts that once vanished, and check corpus parity

Two nets for the same failure: an artifact that fails to parse becomes opaque content, so the record is simply absent and nothing reports it.

TestAwkwardArtifactsSurviveIngest feeds shapes real repositories contain — empty C card, empty J value, empty T value, a titled technote, a forum post with no N card — through parse AND fold, asserting the row lands. Reverting the tokenizer fix fails all three empty-token cases, so the net catches what actually happened.

TestCorpusParity compares a built index against fossil's own tables and is opt-in via QUARRY_PARITY, since the corpora are hundreds of megabytes. Known differences are listed individually with reasons, so anything else fails rather than being tolerated.

It found one on its first run against libfossil: a wiki-tag-propagation-test tag that looks like a page name but is a tag applied to the FIXMEs artifacts. quarry was right and the ground-truth query could not tell the difference — recorded as such. check-in: 86b845671d user: dmestas tags: trunk

23:49
xfer: read the project code with zit stat -R; serve bare stores

zit#85 landed, so the identity of a store is readable without a checkout. ProjectCode drops the clone handshake — free port, spawn zit serve, poll for readiness, POST a container, scrape the push card — for one subprocess that answers directly.

That handshake was the last thing keeping quarry from serving a BARE store: the read plane was already -R-only with no cmd.Dir anywhere, but zit serve is checkout-only. Verified by serving a directory holding nothing but libfossil's .zit — 3,835 check-ins, and timeline, wiki, ticket, forum and technote all answer 200.

Startup also loses a per-repository 5-second worst case. check-in: d7528db81f user: dmestas tags: trunk

19:38
webhooks: the standard outbound surface (0001 Principle 6)

Activity events reach third parties over signed HTTP, so an integrator never has to speak NATS. Subscriptions live in forge OLTP; the signing secret is printed once at creation and never readable again.

Delivery is durable rather than a fire-and-forget goroutine: each (event, subscription) is one JetStream message, acked on 2xx, NAKed with growing backoff, MaxDeliver as the dead-letter boundary. An endpoint down during a deploy still gets its events — proven live by writing with the receiver stopped and seeing the delivery land when it returned.

0003 §3 names dagnats for this class of work; it runs on the JetStream quarry already embeds, so this uses that directly and leaves dagnats as the upgrade path when v3 brings the engine in for CI.

Mentions are not forwarded: they address a person, not a repository. check-in: 550b7af81c user: dmestas tags: trunk

19:31
rfcs: refresh status — accounts, attachments, authored cards

0003's as-built section was five slices behind. Records what shipped and the two findings worth carrying: the JetStream cap silently discarding un-folded events, and the parse failures that fall back to opaque content without surfacing an error. Both were invisible until measured against fossil's own tables, which is the argument for putting that parity check in CI.

0001 §10 no longer lists accounts as remaining; the open v2 items are web sessions, the notify inbox and the webhook dispatcher. check-in: 2ee274084f user: dmestas tags: trunk

19:27
attachments: serve content, gated by the A card

A download route completes the previous slice. The bytes come from 'zit artifact --raw'; the index gates which hashes are servable, so this cannot become read-any-artifact-by-hash, and a deleted attachment stops being reachable by URL. An older revision of a live file still is — fossil keeps attachment history.

The route is hostile to its own payload: always a download, never an inline render, octet-stream regardless of what was stored, nosniff, and a deny-all CSP. Reads are capped at 32MiB and refused rather than truncated. check-in: 0a228d956b user: dmestas tags: trunk

18:34
index: fold attachments (A card)

The last data delta against fossil. quarry parsed the A card into an Attach and dropped it, so attachments were invisible: sqlite showed none of its 11, tcl none of its 3,333. Both now match fossil's own table exactly.

Fossil models attachments as a log, not a set — the newest card per filename wins and a card with no source is a DELETION — so resolving them wrong resurrects deleted files. Real corpora exercise that: 1 deleted on sqlite, 10 on tcl.

Content is not linkable yet. Serving arbitrary attached bytes needs a decided Content-Disposition and CSP posture, so the page names each file and the raw-artifact route stays its own decision. check-in: 4831e9bef1 user: dmestas tags: trunk

18:12
write plane: author the C and N cards zit#77 opened

technote gains a Title field: the C card is what a timeline renders as the note's name, and zit refuses to attach it later, so it is write-once at save time. The edit form prefills it.

forum posts and replies now carry an N card. quarry's form invites markdown, but an omitted card is text/x-fossil-wiki by fossil's rule, so a markdown post was rendering as wiki markup — the same default wiki.save and technote.add already applied.

Tags (--tag) stay unadopted: repeatable inputs need a slice field the ops layer does not encode yet. check-in: 89c60b6ca1 user: dmestas tags: trunk

17:07
oltp: accounts and per-repo roles

0001 §8's v2 tables. Accounts are the web/API identity (argon2id verifier, site-admin flag, read/write/admin per repo); the RFC §6.3 shared secret stays the sync credential, since the protocol fixes its derivation and the server must recompute it.

ops.Auth becomes the required role, so the Admin level that was already declared now means something. The gate stays open until the first account exists, mirroring the sync-user policy, so existing deployments and local single-user serving are unaffected. check-in: bf5ba9d2fc user: dmestas tags: trunk

16:17
artifact: an empty trailing token is legal on any card

Broadens the previous J/T/U fix: fossil's next_token() returns a non-NULL empty string whenever a card ends in a space, on any letter, and each card's own validation decides. C is the common case — 1,009 empty-comment artifacts across sqlite and tcl were rejected and demoted to content. Interior empty tokens stay malformed, and arity still gates the rest. check-in: 5773517bde user: dmestas tags: trunk

15:57
ingest: fold as we publish so a big census can't overrun the stream

ZITEVT is capped with DiscardOld, and watch() published the whole census in one burst — so a repo larger than the cap deleted its own head while the indexer was still folding, then reported itself done. sqlite indexed 41,478 of 131,558 artifacts and lost every ticket, wiki page and forum post. Publishing now yields to folding every 64 MiB. check-in: 44c86387b9 user: dmestas tags: trunk

15:57
artifact: accept an empty trailing card value

Real Fossil writes an empty value as a bare trailing space ("J comment ", "T +date <hash> "). The RFC grants this only for the U card, so quarry rejected the artifacts and demoted them to content — costing sqlite all 2,100 ticket changes. Tolerated on J/T/U only, trailing position only. check-in: 03858409ec user: dmestas tags: trunk

15:08
web: show indexing on the fleet page, and keep SSE alive across patches

A first index can run for minutes, so the home page showed a repo with zero check-ins and no explanation. Repos now carry an Indexing flag and the fleet row wears a badge until the first fold finishes.

The badge exposed a live-update bug that had been there all along: data-init sat on the same element the stream patches. A patch morphs the target to match the fragment, the fragment has no data-init, so the first patch deleted Datastar's own connection attribute and the stream went quiet. The timeline had it too — masked because it only sends on change, so it appeared to work exactly once. The attribute now sits on a wrapper outside the patch target. check-in: 8e2eef5ab2 user: dmestas tags: trunk

13:44
markup: map fossil site-relative links; theme-neutral pikchr ink

Two bugs found by browsing real content on a phone.

Imported wiki/technote/forum bodies are full of fossil's own routes (libfossil's wiki alone: 500 /finfo, 211 /dir, 105 /timeline, 23 /doc). Quarry passed those hrefs through verbatim, so libfossil's home page offered a README.md link that went nowhere. Resolver grows ResolvePath: /doc/<ver>/<path> maps to quarry's file route, /info to the artifact, /timeline /wiki /forum /technote to their pages; a verb quarry has no page for drops the link and keeps the label, since a dead link is worse than plain text. file.get now lets zit resolve symbolic versions (trunk, tip, tags) instead of demanding a hash — that is what /doc links use, and zit is the authority on names. The href rewrite runs BEFORE reference resolution so the two passes never see each other's output.

Pikchr hardcodes rgb(0,0,0), which is invisible on quarry's dark theme — the DOM assertions all passed while the diagram was unreadable, and only a screenshot caught it. Its default ink now becomes currentColor so a diagram follows the page theme; author-specified colours are untouched. Fossil solves this with a server-side --dark-mode flag, which cannot work for a CSS-themed page. check-in: 1d680372e5 user: dmestas tags: trunk

12:53
markup: Pikchr diagrams via WebAssembly (fossil's third markup)

pikchr.c built to a standalone WASI module (-DPIKCHR_SHELL -sSTANDALONE_WASM) and run under wazero, so the CGO-free constraint holds. Its own CLI reads - and writes SVG, so the host ABI is a pipe: no memory marshalling to get wrong. Renders text/x-pikchr artifacts and pikchr fences inside markdown; a diagram that will not compile degrades to its source plus the compiler's message.

SVG does NOT ride the document policy. It gets a narrow allow-list derived by enumerating what the module actually emits (six elements, a closed geometry/paint attribute set, no script/href/foreignObject), so even a hole in Pikchr's own label escaping cannot reach a page. Fence placeholders are alphanumeric with a per-render nonce: an HTML parser rewrites control bytes, so a NUL-based marker does not survive sanitizing (caught by test). Provenance and the rebuild recipe are in the ledger. check-in: 58fe4cf214 user: dmestas tags: trunk

12:45
markup: render Fossil wiki format; fix mimetype dispatch; GFM completeness

An absent N card means text/x-fossil-wiki, not markdown — fossil's wiki_render_by_mimetype is explicit — but quarry rendered those as markdown, mangling 217 of libfossil's 443 wiki artifacts ([target|label] links came out as literal text). Adds a Fossil wiki renderer (the seven rules from fossil's own wiki.wiki: paragraphs, single-level * and # lists, indented paragraphs, [target|label] links through the existing Resolver, verbatim/nowiki, HTML pass-through gated by the sanitizer), dispatches on mimetype exactly as fossil does, and stores forum posts' N card so they dispatch too. Fossil site-relative targets quarry cannot serve render their label as text rather than showing wiki source.

Also GFM completeness: footnotes and auto heading IDs, with the sanitizer policy widened to admit what goldmark actually emits (a footnotes div, not section — caught by test). check-in: 45c96a506b user: dmestas tags: trunk

10:39
web: fit the timeline on phone widths

The card's metadata run was the widest line, so auto table layout preferred it, the table exceeded the viewport, and .tw scrolled sideways — putting the right-aligned check-in/user/branch off-screen on a 390px phone. Stack the meta items under 640px (max-content drops to the longest single item) and allow long comment titles to wrap. table-layout:fixed was tried first and rejected: it mis-sized the graph column to ~225px and squeezed cards to two words a line. Desktop is untouched; the graph lint stays clean at both widths. check-in: b43a9a7730 user: dmestas tags: trunk

10:29
technote: fold the C card and show it as the title

Technotes carry a C card (Fossil's timeline title) but the fold dropped it, so the timeline and listing showed raw 40-hex event ids — libfossil's real technotes read 'Technote: 0a83aac2...' instead of 'Project milestone: merge feature ported in'. Fold the comment, prefer it in the timeline and listing, and add it additively (ALTER, not a rebuild) for indexes created by the older schema. Also corrects a stale '-- C card' note on forum_post.body, which is the W card. check-in: 254d554df8 user: dmestas tags: trunk

10:21
rfcs: v2 write plane — all four record types web-authored check-in: 4acdda438c user: dmestas tags: trunk
10:21
write plane: web-authored forum and technotes (v2)

forum.post/forum.reply/technote.add Command ops on zit#76's verbs, lease-guarded with read-your-writes and activity triggers; new-thread, reply, new-technote and technote-edit forms, all gated. Two correctness points: technote.add widens an event-id prefix to the full id (a prefix would mint a NEW technote instead of editing), and forum.reply shares forum.get's {root} path parameter (differing names on one template is invalid OpenAPI — spec-lint caught it).

Fossil has the same surfaces (technoteedit, forumedit), so these are web faces of existing record types, not invented divergence. Wiki authoring already shipped earlier. Verified live in-browser: thread -> reply -> timeline, and technote edit superseding without duplicating. check-in: 4b68c8aae4 user: dmestas tags: trunk

10:05
untrack fixtures/out clones swept in by addremove (build outputs, not source) check-in: 5e8de4dd50 user: dmestas tags: trunk
09:59
remove stray quarry build artifact from tracking check-in: 6af3019d57 user: dmestas tags: trunk
09:57
chat: drop DAG archival — keep chat ephemeral like Fossil

Reversed 0001 §5's 'durability is the DAG': minting chat into synced, content-addressed forum artifacts writes casual chatter permanently into the DAG, the opposite of Fossil's deliberate chat ephemerality (server-local, auto-purged, never synced). Chat stays live + the 7-day JetStream window; durable archival becomes a future quarry-specific/S3 concern, not near-term. Removes internal/chatarchive, its wiring and flags, events.KV, and the zitcli forum/technote wrappers it used (they return with v2 web-authored forum). Chat itself is unchanged. check-in: 3d68e9b401 user: dmestas tags: trunk

05:17
rfcs: chat forum-archival shipped (zit#76 landed) check-in: db788a07c6 user: dmestas tags: trunk
05:16
chat archival: fold chat into forum-thread artifacts (0001 §5 durability)

internal/chatarchive: batches a channel's messages (per N or T) and mints them as a 'Chat archive' forum thread through zit forum post/reply (zit#76) — one thread per channel, each batch a reply carrying the transcript, so history clones with the repo. Minting runs only on the lease holder; non-holders drop their buffers (the holder's archive folds back to every node). zitcli gains ForumPost/ForumReply/ TechnoteAdd. --chat-archive on by default. Verified live: chat batches materialize as forum threads and reply to the same thread. check-in: bd91af3158 user: dmestas tags: trunk

05:11
zitcli: use diff -R on bare stores (zit#69); re-tighten write ordering (zit#75)

zit diff now accepts -R for --from/--to, so quarry diffs bare stores without a checkout. Millisecond D cards restore real same-second write order, so TestWriteOps asserts strict last-write-wins again. check-in: 76b09e7fb8 user: dmestas tags: trunk

05:04
rfcs: v1 shipped — chat and notify wiring complete check-in: f6f93c81cc user: dmestas tags: trunk
05:04
notify: fire mention and activity triggers to the notify companion (0001 §5)

internal/notify: a Novu-compatible POST /v1/events/trigger client (quarry triggers, notify owns delivery). chat.post parses @mentions and fires quarry-mention per handle; wiki/ticket write ops fire quarry-activity to a repo:<name> topic. All best-effort — a notify outage never fails the user action — with idempotency keys so replays dedup. --notify enables it (disabled by default). Verified live: mention and activity triggers POSTed with the right Novu shape. check-in: 37f62c9a82 user: dmestas tags: trunk

04:50
chat: per-repo live channel over NATS (0001 §5)

internal/chat: messages fan out over NATS with a 7-day JetStream window (replay on join), keyed by project code so a channel is shared across nodes. chat.history/chat.post ops (post is not lease-gated — it mutates no store); web page with on-load history, a plain post form (no-JS baseline), and an SSE append stream (Datastar enhancement). Verified live: form post + cross-connection SSE delivery in-browser, and cross-node fan-out on a shared external NATS. Durable forum-thread archival waits on zit forum verbs (zit#76); the window is interim durability. check-in: 5670d3643d user: dmestas tags: trunk

04:31
read plane: forum threads and technotes (v1 completion)

Fold Forum (G/H/I/W cards) and Technote (E/W cards) artifacts into forum_post/technote tables; forum.list/get and technote.list/get ops; threaded forum pages and technote pages through the sanitizer-gated markup pipeline; both kinds join the unified timeline (y=e|f) and nav. The parser already handled these types, so this is fold+render only. Verified against libfossil's real history: 35 forum threads, 26 technotes render, graph lint clean. check-in: c212383053 user: dmestas tags: trunk

04:06
reindex: --nats support for multi-node cluster rebuild

reindex was embedded-only, so a node's index could not be rebuilt in an external-NATS deployment (the durable position and shared cursor live in NATS, outside a node's --data). --nats routes reindex through the shared plane; the reset republishes the census that every node's fold absorbs. Validated on Linux/hetzner over external NATS. check-in: 87282e7532 user: dmestas tags: trunk

03:49
rfcs: external NATS shipped, retention correction, trio status check-in: 9781c03cea user: dmestas tags: trunk
03:48
scale-out: external NATS + multi-node fan-out (trio part 1)

events.Connect joins an external NATS so N nodes share one plane; --nats selects it (embedded stays the single-node default). Retention switched work-queue -> limits+cap: work-queue load-balanced one copy across nodes, leaving each index partial; limits fans out to per-node durables (name now carries a hashed node id) while MaxBytes/MaxAge keep disk bounded. Lease refusals surface as 503 (ops.ErrUnavailable), not 500. Verified live: two nodes on one external NATS + shared store, each folds the full store, the leased node writes while the other 503s and still folds the write cross-node. check-in: 89e4991bbf user: dmestas tags: trunk

02:34
rfcs: leases shipped; phase 4 complete; zit#74/#75 status check-in: 63ee2a682e user: dmestas tags: trunk
02:34
lease: single-writer coordination (0001 §3)

NATS KV TTL lease per store keyed by project code. Write ops and sync pushes route through Guard: renew-or-acquire, failover grace after displacing a holder, commit-point re-check before the write, and writes-refuse when the coordinator is unreachable (correctness over availability). Single-node holds its own lease with no grace delay; the machinery and failure posture are identical at N nodes. Tests: mutual exclusion, failover, coordinator-down refusal. check-in: d1436c2071 user: dmestas tags: trunk

02:18
security: close whitespace-card sync-gate bypass; write-op input 400s

Audit found a critical: scanRequest matched card keywords with a raw byte-prefix, so a push/login card indented by any Fossil-whitespace byte (SP/HTAB/VT/FF/CR) was invisible to the gate but still honored by the zit backend — an unauthenticated write to a gated repo. Now tokenizes exactly as zit's tokenizeCard (leading-whitespace-skipping), verified live. Also: gate GET edit/new pages (were ungated), reject control chars in wiki names (400 not 500), ticket.set no-fields 400 via new ops.ErrInvalid. check-in: 9db9b2ebae user: dmestas tags: trunk

02:07
rfcs: record shipped auth, retention, write path, interop status

0001 §7/§8: fossil-compatible §6 shared-secret login (not bearer tokens), work-queue stream retention, forge OLTP sync_user table. 0003: phase 0-4 status with cross-fossil interop results. check-in: 4788959dfa user: dmestas tags: trunk

01:05
write path: wiki/ticket ops, forms, Basic auth over sync secrets

wiki.save + ticket.new/set through zit#71 verbs with read-your-writes; edit/new pages; Token ops gate on HTTP Basic against the RFC-derived shared secret (same credentials as sync). Harness stores now carry authored wiki+tickets; wiki surfaces join disposability. check-in: 9005c16485 user: dmestas tags: trunk

2026-08-15
23:03
web: /docs page serving the registry-generated API reference check-in: 216d15e366 user: dmestas tags: trunk
23:02
docs: API reference generated from the registry; staleness gate

quarry docs + make docs; TestDocsCurrent fails when docs/api.md drifts (0003: documentation is a phase exit criterion). check-in: fb2f393a1e user: dmestas tags: trunk

21:51
oltp: drop redundant sqlite embed import check-in: cdb79b6675 user: dmestas tags: trunk
21:50
xfer: push gating with RFC §6 login verification; forge OLTP v1

quarry user add derives the shared secret via a clone handshake (project code, no .zit bytes) and stores it in oltp.db. Gated repos verify body or x-f-l-c cookie logins per request; pulls stay anonymous. Fossil pushes authenticate end-to-end; zit's client stops sending credentials after round 1 (zit#74) so its pushes fail closed until that lands. check-in: bcef58408e user: dmestas tags: trunk

21:30
xfer: sync fronting (0001 §7 v1 slice)

POST /r/{repo} (+/xfer alias) proxies to lazily-started per-repo zit serve children; dead children restart on the next request. E2E: clone through quarry, push back, watcher folds the push. Push auth waits for the forge-OLTP slice; leases stay v-next. check-in: 31b695c5cb user: dmestas tags: trunk

20:56
web: live fleet page; shared SSE patch loop

Home table morphs on any repo's change via Watch("*") wildcard. check-in: 0556d0b12c user: dmestas tags: trunk

20:54
ticket.list: cursor pagination (updated~ticket), default 50 check-in: 6d7151de29 user: dmestas tags: trunk
20:45
web: escape wiki titles in every generated link

tcl's '(Initi/Fin)alize' page broke path routing; %2F keeps a slashed title one mux segment. check-in: 739eb90269 user: dmestas tags: trunk

20:40
wiki: edit history and revision views

wiki.history op + rev param on wiki.get; history page, historical- revision banner, timeline wiki rows link their exact revision. check-in: cadca370c1 user: dmestas tags: trunk

20:36
events: work-queue retention on ZITEVT; migrate old streams

Acked events leave the stream, so disk tracks the in-flight backlog, not store history (was 5.6GB beside sqlite's 218MB). Old-config streams are dropped with their cursors; the census republishes and idempotent folds converge. check-in: 14b0c96b83 user: dmestas tags: trunk

20:26
index: SQL-side ticket list fold; targeted TicketByPrefix

Full-history fold in Go took 1.3s per request on tcl (8.9k tickets). List now newest-updated first. check-in: 3dbf1759f6 user: dmestas tags: trunk

20:19
index: ticket_change(ticket) index; refresh ticket docs once per batch

tcl's ticket corpus crawled at ~20 changes/s: refreshTicketDoc scanned the whole table per change. check-in: 83fc12d336 user: dmestas tags: trunk

19:36
ingest: consumer pending count decides drained, not a fetch timeout check-in: 29aa399eea user: dmestas tags: trunk
19:23
index: scale branch recompute to real stores

Indexed reverse parent edge + tag lookup; recursive walk lands in an indexed temp table before the UPDATE probe. sqlite.org's repo (36k check-ins) hung for 15+ min before; milliseconds path now. check-in: cd81fba74d user: dmestas tags: trunk

19:01
index: WAL + one branch recompute per fold round

Per-batch recursive-CTE recompute was quadratic over bulk ingest; rollback-journal mode locked readers out during folds. Adaptive fold MaxWait so backlog delivery lag isn't mistaken for drained. check-in: 6da2e08171 user: dmestas tags: trunk

18:48
events: 8MB NATS payload cap for large-store artifacts check-in: 881e7d475e user: dmestas tags: trunk
18:33
phase 3: live timeline via Datastar SSE (Principle 3 enhancement)

events.Watch coalesces per-repo change ticks; composition root hands web a Notifier interface (planes stay sealed); timeline.sse re-renders the tl-body fragment through the op path and morphs #tl; vendored datastar v1.0.0-RC.7 (MIT, ledger row) is the page's only script; the no-JS page is unchanged. data-init, not the removed data-on-load. check-in: 263cff2b6b user: dmestas tags: trunk

18:23
reindex: create the per-repo data dir check-in: 83dea2f98c user: dmestas tags: trunk
18:22
reindex verb: drop derived state and replay stores (0001 §5)

Pipeline.Reindex resets tables + deletes the KV cursor, then syncs; refolding streamed events is idempotent. CLI: quarry reindex --repo. check-in: 7124d5e366 user: dmestas tags: trunk

18:13
timeline: unified events — wiki and ticket rows interleave (y= filter)

Union query over checkin/wiki_edit/ticket_change (ticket titles from the folded search doc); composite date~hash cursor; flat event rows with badges, rails passing through; Show selector in controls. check-in: 4681b2fbc4 user: dmestas tags: trunk

18:04
timeline: branch lines one color fork-to-merge, uniform stroke

Look-ahead branch map colors merge rails by the commit they descend to; join curves keep their branch color to the fork; off-page parents render neutral, never a hashed hue for the empty name; drop vector-effect (zoom-variant thickness) for plain 2.5px strokes. Lint now checks color continuity at every cell boundary and stroke uniformity; unit tests pin the dev-line coloring end to end. check-in: 54f1834d26 user: dmestas tags: trunk

17:45
timeline: seamless graph tiling, date-row rail carry, mobile pass

Tail layer at fixed px width (was stretching to 100% — x misalign); head at top:0 with cy=24; date rows render active rails so pills sit beside an unbroken graph; 5px card gaps; mobile keeps vertical card padding so alignment holds. tools/timeline-verify.js is the browser geometric lint: 0 violations on all three demo repos at 1280 and 375. check-in: 994815316c user: dmestas tags: trunk

17:34
timeline: adopt fossil's comment-flow and card metrics

Paragraphs reflow to card width (newlines are not line breaks, fossil semantics); node/time top-aligned via fixed head SVG over stretching tail rails; card padding/radius/detail values from fossil default.css (BSD-2, ledger row added). check-in: 1529a698c5 user: dmestas tags: trunk

17:24
timeline: graph cell min-width (absolute-only content collapsed td) check-in: 81ee450484 user: dmestas tags: trunk
17:21
timeline UX: fossil-modern cards, continuous rails, controls

Card layout with title/body split and bottom-right metadata; date pills; stretchable rail SVG + fixed node dot (rails continuous through tall rows, leaf tips as rings); Max control with fossil's n= name. check-in: a74b77467d user: dmestas tags: trunk

17:04
timeline: fossil-shaped rows + server-side SVG DAG graph

Day dividers, HH:MM times, comment cell with metadata tail and leaf/merge badges, per-branch colors (cleanroom hash_color), rail assignment after graph.c's shape rendered as per-row inline SVG — no JavaScript. check-in: bdb20a06eb user: dmestas tags: trunk

16:56
fix: timeline truncated at first row (web entry missing Branch)

Template errors now buffer-render to a logged 500 instead of silent truncation; populated-timeline regression test added. check-in: 81642956b5 user: dmestas tags: trunk

16:50
branch labels: tag facts + wholesale recompute, timeline filter

Manifest/control T cards fold as dated tag rows; branches recomputed per batch via recursive CTE (order-independent; latest tag wins); timeline branch column + ?branch= filter; checkin page shows branch. check-in: 3e10e3f43b user: dmestas tags: trunk

16:41
rfc 0003: pin fleet-registry alignment with zit#70

Flags stay ephemeral until zit's global db; OLTP registry is forge metadata only, never store locations; #59+#70 unblock bare stores. check-in: 342f8f00b5 user: dmestas tags: trunk

16:38
fleet: multi-repo serving, /r/{repo} routes, pagination

repo.list op + fleet home; per-repo indexes and pipelines on one NATS; repeatable --repo name=path; timeline before= cursor with older-link; harness runs two stores and asserts isolation. check-in: 0b245645c7 user: dmestas tags: trunk

16:15
highlight: dark theme covers every light token class (030)

Classes github-dark omits get explicit overrides at the dark base color; regression test pins full coverage incl. nx/p/na/nb/bp. check-in: 4fe9a80214 user: dmestas tags: trunk

16:15
layout: persistent nav, code-block styling, single h1, a11y (010/100/110/120/140)

Header nav + search form in base; markdown headings demoted one level; pre gets --code-bg; tables scroll in .tw wrappers; th scope attrs. check-in: 3253bba7bb user: dmestas tags: trunk

16:13
search fixes: folded ticket docs, mark snippets, honest limit (050/060/080/130)

search_doc gains label; tickets one folded doc per id; ⟦⟧ markers become <mark> post-escape; default= validate rule fills absent keys. check-in: 839e3e756a user: dmestas tags: trunk

16:10
error taxonomy: 404s, themed error pages, message hygiene (tickets 020/040/070/090)

ops.ErrNotFound sentinel; router maps to 404, hides internals on 500; JSON 404 for unmatched API paths; themed web 404/500; zitcli.ErrNoEntry; clean codec parse messages; shunned-diff note. check-in: f073a79918 user: dmestas tags: trunk

14:30
search: FTS5 over check-ins, wiki, tickets

External-content fts via triggers on search_doc (immutable-artifact upserts stay idempotent); token-quoted queries; /search page + op; required-input ops get explicit harness surfaces. check-in: c650d85c33 user: dmestas tags: trunk

12:20
diff: checkin.diff op + colored diff page (zit diff --from/--to)

Root check-ins report their file set as all-adds; zit#69 filed for diff -R; per-checkin diff surfaces in harness. check-in: bb37910fbb user: dmestas tags: trunk

12:16
highlight: chroma wrapper, fenced-code + file-view highlighting

Cleanroom (their wrapper is settings-coupled); classes only, both themes in one stylesheet; sanitizer learns chroma token classes. check-in: 689d0692d7 user: dmestas tags: trunk

12:11
files: zit cat behind file.get op and /file page

Rest-wildcard route; binary detection keeps bytes out of JSON; tip-file surfaces in harness; spec paths drop the ... spelling. check-in: 0821146a78 user: dmestas tags: trunk