Skip to content
quarry Artifact [5ffa5c2d3d]
Login

Artifact [5ffa5c2d3d]

Artifact 5ffa5c2d3d7bf31ea5f1813721bc6533f468506864679ac9a31717ebd24e82e5:


0003 — quarry implementation plan

Companion to 0001-quarry-forge.md (the spec) and 0002-forgejo-salvage.md (the UI salvage map). This document owns what the spec deliberately does not: concrete technology choices, build order, testing strategy, and observability wiring. Decisions here are revisable without touching the spec; decisions that would change the spec's architecture go back through 0001.

0. Guiding constraints (inherited, not restated)

1. Build order

The organizing rule: the spec's riskiest claims get proven by a tracer bullet before any breadth work, and the three enforcement mechanisms (disposability harness, ops registry, provenance ledger) are built first, because retrofitting a merge gate onto an existing codebase never works.

Phase 0 — rails before features

Exit: CI runs all three gates; a dummy op renders a dummy page and appears in a generated OpenAPI document.

Phase 1 — the tracer bullet

One store → watcher (validated-prefix fold via the zit subprocess, zit#17 stat-and-reopen discipline from the start) → embedded NATS event → per-node SQLite fold → one op (timeline.list) → one HTML page and its generated API endpoint, footer with versions and render timing.

Proves the four load-bearing claims at minimum cost: log-as-event-stream, embedded-index-as-derived-state, registry-generates-spec, disposability-as-gate.

Phase 2 — read plane breadth

0002 §4's extraction order: markup first (cleanroom sanitizer + conformance corpus before anything renders) → base layout → fleet → repo home/files → timeline/check-in → diff stack → graph → wiki render → search (FTS5) → activity/stats. Each page lands against the per-page definition of done (0002 §4 item 5) and grows the disposability snapshot set incrementally.

Phase 3 — liveness

Datastar SSE wiring, chat transport + forum archival, notify integration. First real test of Principle 3's refresh-fallback discipline.

Phase 4 — sync fronting + leases

/xfer fronting, then the 0001 §3 lease machinery. Deliberately last in v1: it is the only distributed-systems-hard part, and everything before it works single-node.

v2/v3 then follow 0001 §10's staging. Each phase ends with the harness green and a docs/ page generated from the spec — documentation is a phase exit criterion, not a v-final activity.

Status (as built)

Phases 0–4's single-node scope is shipped and dogfooded from commit zero in a zit store. Delivered beyond the original slice boundaries:

Phase 4 is complete. The remaining v1 horizon is the scale-out trio:

  1. External NATS URL (shipped): --nats <url> joins an external cluster so N nodes share one event plane; the embedded server stays the single-node default. This forced the retention correction above (work-queue → limits+cap) so every node folds the full stream, and per-node durable names so nodes don't collide on one consumer. Proven live: two nodes on one external NATS over a shared store — each folds the whole store, the lease-holding node writes while the other returns 503 and still folds that write cross-node off the shared stream.

The cap has a corollary that cost real data before it was found: ZITEVT discards OLD messages, and watch() published the whole census in one burst, so a repository larger than the cap deleted its own head while the indexer was still folding it. The index then drained what remained, saw NumPending == 0, and reported itself done. sqlite indexed 41,478 of 131,558 artifacts that way and lost every ticket, wiki page and forum post with no error surfaced. Publishing now yields to folding every 64 MiB, so the stream applies backpressure instead of dropping; the regression test reproduces the original loss against a deliberately small stream.

  1. Shared S3-backed store access (ZeroFS/9P per §storage): no quarry code — --repo already takes a path, and a 9P mount is just a path. The work is deployment (one ZeroFS process per bucket, supervised; 9P not NFS for the fsync durability contract zit's log.commit() depends on) and validation on a Linux+9P host, which this dev Mac can't exercise.
  2. Parquet interchange lake: deferred by the tech-choices table ("no Parquet dependency in v1"; when the lake lands, write via a query engine's COPY TO, not a standalone library). It optimizes new-node hydration (read the lake instead of replaying every store) — an efficiency layer over a system that is already correct without it.

The trio turns the single-process build into N stateless read nodes plus one leased writer. Part 1 is the code; parts 2 and 3 are deployment and a v-next efficiency layer.

An adversarial audit of the write surface (5 dimensions, per-finding verification) hardened it: it caught and we fixed a whitespace-card sync-gate bypass (the gate now tokenizes exactly as zit does), ungated edit-form GETs, and two input faults that 500'd instead of 400ing.

Interop and scale findings surfaced upstream zit work, all landed: authoring verbs (zit#71), the sync client's per-round credential gap vs. the protocol's own §6.5 cookie-login MUST (zit#74 — authenticated zit push authenticates through quarry's gate end-to-end), a bare-store -R gap in zit diff (zit#69), whole-second D-card resolution causing same-second writes to fold in hash order rather than time order (zit#75), the technote/forum cards the verbs could not write (zit#77), and zit frames reconstruction cost (zit#84 — a plaintext memo plus parallel batch verification, ~31% off a cold sqlite read).

Two remain open and shape what quarry can do next:

The lesson quarry keeps re-learning from these is in the failure mode, not the bugs: an unparsable artifact falling back to opaque content is silent by construction. Parity against fossil's own tables — event, ticket, forumpost, attachment — is the only check that catches it, and it belongs in CI against a real corpus rather than in a person's memory.

2. Technology choices

Each entry: choice, rationale, escape hatch. These are ADR-grade decisions recorded compactly; split any into a standalone ADR when it accretes real debate.

Area Choice Rationale Escape hatch
SQLite driver ncruces/go-sqlite3 Upstream SQLite compiled to Wasm (wazero): real SQLite behavior, fast tracking of releases, no CGO (cross-compilation stays trivial), FTS5 enabled, and a Go-implementable VFS surface — the scale-out hook if node indexes ever want S3-backed hydration. Preferred over modernc (transpiler artifacts, historic concurrency edges) and mattn (CGO). Known costs: Wasm boundary tax on write-heavy paths (fits our read-dominated profile), amd64/arm64-only compiler (interpreter elsewhere), ~tens-of-ms startup compile, narrow bus factor (mitigated: unmodified upstream SQLite makes a forced move driver-swap-shaped, not data-shaped). mattn (CGO) or modernc; schema is plain SQLite either way
HTTP router stdlib http.ServeMux (1.22+ patterns) The ops layer already does what chi would add; a router dependency is a shallow module here. chi, mechanically
Ops registry Hand-rolled on stdlib + generics (~1.5k lines total) The registry is quarry's spine; you don't build the central abstraction on someone else's framework. The flat, form-expressible struct constraint (0001 §9) makes the reflector genuinely small. Pieces: generic Register[In,Out] over a descriptor map (~150 lines, explicit RegisterAll() wiring); JSON codec (stdlib); form/URL-values codec (~150 lines, needed for HTML regardless of framework); path-param binding onto ServeMux wildcards; OpenAPI reflector (~300–400 lines); tag-driven validator shared by both codecs; instrumentation wrapper around Run. Huma (danielgtaylor/huma) — our Run signature is already its handler shape, so migration is mechanical
OpenAPI document types Reuse dagnats' OpenAPI 3.1 synthesis types (currently internal/openapi in dagnats — promotion to a public package is a prerequisite, see §5) One shared, tested document-assembly layer; quarry writes only the struct-reflection front half; both projects emit consistent 3.1 documents. Hand-roll the back half too (~200 more lines)
Spec correctness gate Off-the-shelf OpenAPI validator lints the generated spec in CI (dev-time tool, not a runtime dep); every registered op's fixtures round-trip through both codecs Closes "our generator emits a subtly invalid spec" without a runtime framework. —
Markup goldmark + bluemonday + chroma Per 0002 §2; sanitizer policy is cleanroom with the two-lineage conformance corpus + fuzz target. —
Embedded NATS nats-server as a library The dagnats/notify pattern, already proven in this stack. External NATS URL (already the scale-out flag)
litestream Supervised sidecar firestorm-proven shape; embedding buys nothing. —
Parquet writing Deferred with the interchange lake (0001: scale-out) No Parquet dependency in v1 at all. When the lake lands, prefer writing via a query engine's COPY TO over a standalone parquet library — decide then. —
Frontend Datastar (only default-allowed framework, 0001 Principle 2) + html/template Per 0001/0002. Explicit per-case exception process
zit access Subprocess boundary — the zit binary is the only store interface, reads and writes (0001 §2) zit is Zig, quarry is Go; the versioned CLI is the whole contract, so releases decouple and quarry stays CGO-free. Most page data serves from quarry's indexes; on-demand reads (file-at-checkin, diff, annotate) pay ~1–5ms spawn, off the hot path and behind caching. Quarry's CI tests against pinned zit versions and each release declares a minimum zit version. C-ABI read library exported from zit — revisit only with profiling evidence

3. dagnats integration

dagnats provides durable HTTP endpoints (HTTP trigger → event-sourced run → engine-resolved respond step) with OpenAPI 3.1 synthesis from trigger defs + declared schemas. Division of labor:

4. Testability — one strategy per risk class

5. Observability

Lean on dagnats: quarry imports dagnats' observe package for OTel exporter wiring, resource attributes, and conventions rather than building its own setup. Command ops propagate W3C trace context into the workflows they launch (dagnats' bridge already speaks tracecontext), so one stitched trace runs from HTTP request through the durable run.

Quarry adds only what dagnats cannot see:

6. Prerequisites and upstream work

7. Risks