Skip to content

Releases: javimosch/grange

grange v0.13.3 — HEAD no longer answers 401 on the public front door

Choose a tag to compare

@javimosch javimosch released this 31 Jul 14:08

curl -I https://grange.intrane.fr/llms.txt returned 401 while curl returned 200. A HEAD request matched no GET route, fell through to the auth gate, and reported the front door as locked — to link checkers, uptime probes, cache validators and every cautious client, while a plain GET worked.

HEAD is now GET without the body (RFC 9110) across all routes: the Content-Length still describes what a GET would return, no body follows, and gating is unchanged (an anonymous HEAD on /count is still 401).

Nothing in 22 harnesses and 388 assertions had ever issued a HEAD. make routes does now.

Found by a new live-service contract audit that reads each deployed service's own /llms.txt and probes what it advertises: stranger/contract.sh.

grange v0.13.2 — honest telemetry (and the fifth CLI spec)

Choose a tag to compare

@javimosch javimosch released this 30 Jul 21:09

grange now sends one anonymous usage event per invocation, and this release note is where you find that out — not a packet capture.

{"tool":"grange","version":"0.13.2","event":"install","verb":"count",
 "os":"linux","arch":"x86_64","exit_class":0,"ts":1785442906}

That is the entire payload. No install id — the install event fires once per machine, so counting those events answers "how many machines" without anything per-install existing. Never sent: hostname, username, paths, arguments, flag values, database or collection names, tokens, document data, error text, environment.

Off, checked before any network code runs: GRANGE_TELEMETRY=0, DO_NOT_TRACK=1, grange telemetry --telemetry-off, and CI is detected and defaults to off. grange telemetry prints the real next payload, built by the same function that sends it. A hung collector costs 2 seconds once and cannot change your exit code, output or data.

Why: for 51 milestones there was no way to distinguish "nobody uses this" from "the documented install command has been broken since the first release". v0.13.1 established it was the second. Downloads report the same zero for both.

Also new: grange telemetry verb, a public POST /t collector (aggregate counters, no IP retention) so you can run your own with GRANGE_TELEMETRY_URL, and make telemetry — 25 checks, most of them about what it must not do.

The full contract is cli-telemetry-spec, published alongside this as the fifth agent-first CLI spec. grange is its reference implementation.

grange v0.13.1 — the documented install command works now

Choose a tag to compare

@javimosch javimosch released this 30 Jul 19:58

The install command in the README had never been run by anyone, including me. It pointed at an asset name (grange-linux-x86_64) that no release has ever published, so it returned 404 — 9 bytes of "Not Found" that chmod +x happily accepts.

And the binary it pointed at was dynamically linked against libssl, libcrypto, libsqlite3 and glibc 2.34+, under a README paragraph promising the opposite. It would not have started on Debian 11, Ubuntu 20.04, RHEL 8, Alpine or a slim container.

This release ships a genuinely static musl binary (7.5 MB, no glibc floor, runs on FROM scratch), published under the documented name. make release refuses to produce anything else, and make stranger walks the published artifact from an empty directory with no repo and no toolchain — which is how both defects were found.

grange v0.13.0 — the documentation claim becomes a check

Choose a tag to compare

@javimosch javimosch released this 30 Jul 13:11

grange is agent-first: grange guide and the in-repo skills are the interface, so documentation drift is a functional bug — and this project's history is almost entirely that bug, always found by accident.

make doccoverage enumerates the dev-facing surface out of the source — 16 verbs, 22 routes, 12 operator-facing GRANGE_* variables, 22 flags, every where operator — and fails on anything the guide does not document. With make guide (is it truthful) both directions are now closed.

It caught two real gaps on its first honest run: the guide never stated the pagination cursor's format, and the SDK README never documented the filter language it passes through. The README's own "tests (69)" was 388.

The production-readiness verdict — including adoption being approximately zero, the single-writer ceiling, and an unexplained crash-harness flake — is at the end of docs/OPERATIONS.md.

grange v0.12.3 — the startup log told the truth about a failure it had already reported as success

Choose a tag to compare

@javimosch javimosch released this 30 Jul 12:53

The {"event":"listening"} line was printed before the socket was bound, so a port collision logged a successful start and then exited 1. It is now announced after the bind.

Found via two systemd units sharing port 8802 on the hosted instance, one of which had been restarting every two seconds indefinitely with no alert anywhere — /health is in-process and /ready knows nothing about sibling units. scripts/readycheck.sh now asks systemd whether any grange unit is failing or looping and folds it into the same alert (confirmed with NRestarts, so it does not fire on every deploy).

grange v0.12.2 — OR clauses (as alternatives) and a restore drill

Choose a tag to compare

@javimosch javimosch released this 30 Jul 12:39

where was AND-only. field=a|b|c now matches ANY of the alternatives — an IN clause, served as a union of index buckets rather than a scan, on both the hot and cold storage layers (mode comes back indexed-union / cold-index).

It is deliberately not arbitrary boolean OR across different fields: the comma is always AND, and every alternative stays an exact index lookup, which a general OR would not.

Two paths that would have returned a confident wrong number are guarded: the O(1) count register, and the cold range index (whose lookup takes a single value — alternatives on a --range field fall back to a verified scan).

Also scripts/restore_drill.sh: the nightly artifact is now actually restored and compared, collection by collection including tenants, rather than assumed restorable. Drilled on production: 8 collections, no drift, writable after restore.

v0.12.1 — a mistyped route now fails

Choose a tag to compare

@javimosch javimosch released this 30 Jul 11:39

/counter used to return a count.

req.path carries the query string, so the obvious way to match a route was has_prefix(req.path, "/count") — and that also matched /counter and /countXYZ. Both returned a perfectly plausible count.

For an agent-first API that is worse than an error: it teaches a wrong URL by rewarding it. An agent that mistypes gets a real answer, writes the wrong URL into whatever it is building, and never finds out.

A route now matches its exact path, or that path followed by ?query — nothing else. All 24 route matches go through one helper, so the policy is stated once.

before now
/count, /count?coll=c 200 200
/counter, /countXYZ 200 with a count 404
/findx, /readyx, /usagex 200 404
/healthzzz 404 404

That last row is why it hid: /health already matched exactly, so the codebase looked like it had a policy.

How it was found

Not by inspection. A negative control in the new journey harness renamed a documented route to /counter, to prove the harness could detect a contract advertising something that does not exist. The harness passed — because /counter genuinely answered. The control refusing to fail was the bug report.

make routes now probes eleven mistyped routes alongside the real ones, and reverting a single route to the loose match makes it report GHOST /counter answered instead of 404.

Binary: linux x86-64, statically linked. Behaviour change: if you were (accidentally) relying on a prefix-matched URL, it now 404s.

v0.12.0 — readiness, not just liveness

Choose a tag to compare

@javimosch javimosch released this 30 Jul 08:09

/health says the process is alive. /ready says the database is fit to serve.

The external monitor here polls /health every 60 seconds. That answers "up" as soon as the accept loop is running — and stays green while the data directory has gone read-only, recovery quietly dropped chunks, RSS is about to trip the watchdog, or the nightly backup stopped weeks ago. It would not have noticed that this project's release notes claimed nightly backups for eleven days while there were none.

curl -s "$URL/ready" -H "authorization: Bearer $TOKEN"

Returns 503 when any of those holds, and lists what is failing. It reads no pages and runs no verify, so it stays cheap enough to poll — a readiness probe that costs real work is what eventually takes the server down.

Also in this release: backups (scripts/backup.sh — copy, verify with grange verify, prune; a corrupt source exits 92 and the failed copy is kept), and the marker they record on success is what makes staleness detectable at all.

Choices worth stating

  • /health stays trivial. Two endpoints, two purposes.
  • A missing backup marker reports null, not a failure. Plenty of deployments have no backup job, and a false alarm teaches people to ignore the endpoint.
  • /ready needs a token. It reports RSS, version, collection count and backup age — operational detail, not something to publish on a paid multi-tenant service.
  • Alerting fires on transitions only, reports recovery, and distinguishes down (no response) from failing (responding, sick). A message every 15 minutes while something is wrong gets muted, which is worse than no alert.

An agent can use /ready to decide whether to trust a database it did not deploy.

Binary: linux x86-64, statically linked. Additive — nothing changes unless you poll the new endpoint.

v0.11.1 — backups that exist

Choose a tag to compare

@javimosch javimosch released this 30 Jul 07:56

Backups, for real this time.

The v0.9.0 notes said "Nightly backups". There were none — no timer, no cron, no backup directory on the host. That note has been corrected in place; this release is the mechanism actually existing.

scripts/backup.sh --db /data/db --out /backups/grange --keep 7

Copy, verify, prune — in that order.

A plain file copy is a valid backup while the database is being written, and that follows from the format rather than from luck: every .grg file is written exactly once and never mutated, cold manifests are written last, and recovery drops a torn final chunk. So a concurrent write can cost the in-flight commit and nothing else. Measured under continuous writes: source 165 documents, restored 162, every one whole and the restore writable.

What makes it a backup rather than a copy is verifying it. Every collection is checked with grange verify. A corrupt source exits 92 and the failed copy is kept for inspection — deleting the evidence of a failed backup is how you find out much later — and pruning happens only after a good backup exists, so a run of failures never empties the retention window.

Tenant databases live in a sibling <db>.tenants directory, so the obvious cp -a $DB omits every paying customer. Both are taken, laid out as <stamp>/<name> and <stamp>/<name>.tenants so restoring is a copy back into place.

make backup asserts all of it under continuous writes. Verified against production: the restored copy of the live analytics mirror counted 1160 documents against the source's 1160, intact.

The guide command and the grange-operate skill both document it, per this project's rule that a dev-facing feature lands in the guide in the same change.

v0.11.0 — field projection

Choose a tag to compare

@javimosch javimosch released this 29 Jul 18:16

Ask for the fields you need.

A caller could already say which rows (where), what order (order), how many (limit) and where to resume (after). The one thing they could not say is which fields — so every response carried whole documents. For a database whose consumer is usually a model paying per token, that was the expensive gap.

grange find --db d --coll events --order ts --desc --limit 50 --fields ts,site,path
curl "$G/find?coll=events&limit=50&order=-ts&fields=ts,site,path" -H "$A"

Measured on the live analytics mirror, whose events carry 11 fields: asking for the 3 a dashboard needs took 5 rows from 1255 bytes to 439 — 65% smaller, on every request.

Rules, chosen so a projected row is never a trap

  • id is always returned. A row you cannot fetch again is worse than a large row.
  • A field the document lacks is omitted, not null. grange does not invent structure, and machin's parse() treats a missing field and a null field differently.
  • A dotted path projects under its full path (user.id), flat — rebuilding {"user":{"id":…}} is ambiguous the moment two paths share a prefix.

It composes with everything: ordering, cursors, both storage modes, and export.

Verification

make projection compares projected answers against full answers reduced independently, across both storage modes and five query plans, plus id presence, omitted-vs-null, dotted paths, projected pages walking every row exactly once, and that the payload actually halves. The negative control — making single-field projection silently return full documents — fails four of those comparisons.

All four SDKs take fields in the repository. Correction: when this release was published I wrote that as though it were installable — it is not. The versions on npm and PyPI (grange-db 0.11.0) and the newest Go tag (sdk/go/v0.10.0) predate projection, and also predate ordering (v0.10.0) and keyset pagination (v0.10.0). SDK support for all three ships in grange-db 0.12.0 / sdk/go/v0.12.0; until then, use the HTTP API directly — ?fields=, ?order=, ?after= work against this binary regardless of SDK version.

Binary: linux x86-64, statically linked. Upgrading from v0.10.0 requires nothing; projection is additive and off unless asked for.