Skip to content

Tags: javimosch/grange

Tags

v0.13.3

Toggle v0.13.3's commit message
HEAD is GET without the body — it was answering 401 on the public fro…

…nt door

`curl -I https://grange.intrane.fr/llms.txt` returned 401 while `curl` returned
200. A HEAD matched no GET route, fell through to the auth gate, and answered
"unauthorized" — including on /llms.txt, the one route the contract promises is
readable by anyone with no credential.

So every link checker, uptime probe, cache validator and cautious client was
told the front door was locked, while a plain GET worked fine. The lie was told
specifically to the careful callers.

Nothing here had ever issued a HEAD. 22 gate harnesses, 388 assertions, a
journey test that walks the whole product path, and a route test that probes 11
mistyped URLs — none of them used a method other than GET or POST.

srv_method() maps HEAD to GET for routing across all four route files (14
checks), and srv_write_res drops the body while keeping the Content-Length that
describes what a GET would return, per RFC 9110. Gating is unchanged: an
anonymous HEAD on /count is still 401.

routes_test.sh now asserts HEAD agrees with GET on five routes and that no body
follows the headers, verified by a raw socket read. The negative control fires
on four of the five (/health has no method check at all, so it was never
affected).

Found by a new live-service contract audit (stranger/contract.sh), which reads
each deployed service's own /llms.txt and probes what it advertises — 82 routes
across 8 services. grange and cuzz were the two that disagreed with themselves.

v0.13.2

Toggle v0.13.2's commit message
M51 (part 2): telemetry, because zero downloads meant nothing

Reference implementation of cli-telemetry-spec, the fifth agent-first CLI spec,
written alongside this: https://github.com/javimosch/cli-telemetry-spec

After 51 milestones there was no way to tell "nobody uses grange" from "the
documented install command has been broken since the first release". Both report
zero downloads. It was the second. Clones say 308 from 162 uniques against 5
unique page views, which is mirrors and CI, not people. An agent-operated tool
has no human who will ever file an issue about any of it.

What is sent, per invocation, after the user's result is on stdout: tool,
version, event (install|run|error), verb name, os, arch, exit class, timestamp.
The fields are written out literally rather than serialised from a struct, so
one cannot join them by accident later.

There is NO install id. The spec permits a random one and calls omitting it the
safer default; grange does not need it, because the `install` event fires once
per machine and COUNTING those events already answers "how many machines" with
nothing per-install existing anywhere.

Never sent: hostname, username, paths, arguments, flag values, database or
collection names, tokens, document contents, error text, environment. grange
holds other people's data — the moment telemetry can see any of it, the feature
is indefensible.

Off before any network code runs: GRANGE_TELEMETRY=0, DO_NOT_TRACK=1,
`grange telemetry --telemetry-off`, and CI (detected, default off — this
project's own gate would otherwise out-report its entire userbase from one
`make verify`). `grange telemetry` prints the REAL next payload from the same
builder the sender uses, so a reviewer verifies rather than trusts.

machin's https_post has no timeout, so the send races a 2s sleep across two
goroutines: a hung collector costs two seconds once and cannot change the exit
code, the output, or the data. Verified at 2003ms against a collector that reads
the request and withholds the response — the first version of that test used a
listener that never read, which returns in 2ms down a different code path and
"passed" while testing nothing.

The collector (POST /t, public, therefore forgeable and a usage signal rather
than an audit log) stores day/version/event/os/verb COUNTERS, never a row per
sender, and reads nothing about the connection.

Two bugs found before release, both of the kind that matter here:

  · the opt-out silently failed to persist — $HOME/.config does not exist on a
    fresh machine and machin's mkdir is one level, so `--telemetry-off` printed
    "disabled" and would have kept sending. It now writes, reads back, and
    FAILS with exit 90 rather than claiming success it did not achieve.
  · the collector's counter never incremented while answering {"recorded":true},
    so five identical events became five documents each saying n=1.

Also: the landing had no analytics at all, so arrivals and bounces were
indistinguishable — vigie's cookieless snippet is on it now, and the site is
registered.

v0.13.1

Toggle v0.13.1's commit message
M51: the install path in the README had never once been run

Release downloads across every version: zero. That is not only an adoption
number — it means the install path had no users, so it had no bug reports, and
two defects had been sitting in the first paragraph of the README since the
first release.

The documented command downloads `grange-linux-x86_64`. Every release has
published the asset as `grange`. So `curl -sSL -o grange <url>` wrote 9 bytes of
"Not Found", `chmod +x` accepted it, and `./grange guide` answered
"Not: command not found". That is a stranger's entire first experience.

And the binary it pointed at was DYNAMICALLY linked — libssl, libcrypto,
libsqlite3, glibc 2.34+ — under a paragraph promising "statically linked, no
runtime dependencies, no glibc floor, runs on any linux x86-64". It would not
have started on Debian 11, Ubuntu 20.04, RHEL 8, Alpine or a slim container.
Nothing in grange's source uses SQLite; machin links it into every binary
unless you pass --static, which this project never did.

`make release` now builds with --static and REFUSES to emit a binary that is
not static, has dynamic dependencies, or does not run. 7.5 MB instead of 321 KB.
Insert throughput is unchanged: 104-130k/s either way, measured three times
alternating — the first reading suggested musl was 4x slower and was noise from
a concurrent build, which is why it was repeated before being believed.

scripts/stranger_test.sh walks the published artifact from an empty directory
with no repository, no machin and a throwaway HOME: download by the documented
URL, execute, assert the linkage the README claims, read the guide, run the loop
it prints, start the server, hit the HTTP API, and check that a wrong token
produces an error naming the problem. Against the current release it fails 13
checks. It is deliberately not part of `make verify` — it tests what is
published, which does not exist yet at verify time.

Two of its own checks were caught passing on the 404 page: `ldd` reports "not a
dynamic executable" for a text file and `objdump` finds no GLIBC symbols in one,
so a 9-byte error page scored "static, as the README promises". They are now
gated on the binary actually running.

v0.13.0

Toggle v0.13.0's commit message
M50: "100% documented for agents", as a check instead of a claim

grange has no UI, so `grange guide` and the in-repo skills ARE the interface,
which makes documentation drift a functional bug. This project's history is
almost entirely that bug: a guide reporting version 0.1.0 for ten milestones,
"no fsync builtin yet" long after fsync shipped, "nightly backups" with no
backup job, "all four SDKs take fields" against packages that had none. Every
one was found by accident.

scripts/doccoverage_test.sh enumerates the dev-facing surface OUT OF THE SOURCE
— 16 CLI verbs, 22 HTTP routes, 12 operator-facing GRANGE_* variables, 22 flags,
every where-clause operator — and fails if any is absent from the section of the
guide that should carry it. With `make guide` (is it truthful) that closes both
directions: nothing documented is missing, nothing present is undocumented.

The first version passed all ten checks immediately. That was not completeness:
it grepped the whole guide JSON, where "get" occurs in a dozen unrelated
sentences, so it could not fail. Anchoring each lookup to its owning section made
three of four sabotages fail correctly — and the fourth exposed the check itself.
Testing for a bare "|" passed with every mention of alternatives deleted, because
the pagination cursor format is `<value>|<id>`: the character was present,
documenting something else entirely. Anchored per-operator, it then found a
genuine gap — the guide had never stated the cursor's FORMAT, only that a cursor
existed. Now documented, along with the where-syntax note the SDK README lacked.

The README claimed `make verify` ran "tests (69)". It runs 21 harnesses and 388
assertions. Found while writing the section that brags about catching drift.

And the verdict the milestone run was aimed at, in docs/OPERATIONS.md, with the
not-ready parts named: adoption is approximately zero and every claim here is
measured on a workload I wrote myself; serve is single-writer; memory is bounded
by a watchdog that restarts rather than prevents; one crash-harness flake from
M33 has never reproduced and I destroyed its output. Suitable for a service
whose failure you can tolerate — and until someone other than its author runs
it, "production ready" means ready for this production.

v0.12.3

Toggle v0.12.3's commit message
M49 (part 2): the server announced "listening" before it had bound

The startup line was printed before listen(port), so a port collision logged
{"event":"listening","port":8802,...} and then exited 1. Anything reading that
log — a supervisor, an agent, a person — was told the server was up by the
process that had just failed to start. Announced after the bind now, and proven
by starting two servers on one port.

That was found because dk1 had TWO systemd units with the same ExecStart on port
8802: grange-read held the port and grange-follower had been failing and
restarting every two seconds for as long as both existed. Nothing alerted, and
nothing could have: /health is in-process, /ready knows nothing about sibling
units, and the external monitor only ever polled the healthy one. The duplicate
is disabled on the host (grange-read is the one to keep — it carries the RSS
limit and the scan budget the bare duplicate lacked).

So readycheck.sh now asks systemd whether any grange unit is failing or looping,
and folds that into the same transition alert. `activating` is normal for a
second after a restart, so it confirms with NRestarts rather than alerting on
every deploy. The systemctl call is overridable via $SYSTEMCTL and was exercised
both ways with a stub — a restart-loop check nobody has seen fire is a check
nobody has tested, and five of this project's own negative controls turned out
to be broken.

Version 0.12.3 rather than amending v0.12.2: that tag's release asset is already
published, and leaving the binary in production different from the binary people
download is the kind of small divergence that costs an afternoon later.

v0.12.2

Toggle v0.12.2's commit message
M49: OR, as far as it can go without lying — plus a drill

`where` was AND-only, so "status is active OR pending" was not expressible and
callers were running two queries and merging client-side.

This adds ALTERNATIVES on an equality clause — `status=active|pending` matches
any of them, an IN clause. Not arbitrary boolean OR across different fields:
that needs an expression tree and would change how clauses combine, whereas
alternatives cannot be confused with the comma that already means AND and, the
reason this shape was chosen, each alternative stays an exact index lookup. The
clause is served as a UNION of index buckets on both storage layers — hot
in-memory buckets and cold hash-partitioned index pages, which are different
implementations; the first version worked on hot and silently answered 0 on cold.

Two paths would have returned a confident wrong number rather than failing, and
both are now guarded: the O(1) count register (one bucket cannot answer a union,
and the bucket for the literal "a|b" answers 0), and the cold range index, whose
candidate lookup takes a single value — handing it "a|b" dropped every matching
document. Alternatives on a --range field therefore fall back to a verified
scan: slower, never wrong.

Removed cold_idx_usable and cold_idx_count_only. Nothing in the repository
called either — superseded by the multi-clause planner and never deleted — and
an uncalled index path is where the next union bug would have hidden unseen.

Also: scripts/restore_drill.sh, because `make backup` proves the backup SCRIPT
works against a database it just built, not that the artifact sitting on the
host can be restored. It restores the newest real artifact into a temp
directory and compares every collection, admin and tenant, by count and
integrity, then asserts the result is writable. On dk1: 8 collections, no
drift. Its first run cried wolf over a tenant created after the backup ran, so
it now compares against what existed when the artifact was taken.

scripts/inclause_test.sh covers both layers, and each expectation is computed
independently rather than read back from the binary. Verified by two negative
controls — removing the hot union and ignoring cold alternatives — each of which
fails only on the layer it sabotages.

Documented in `guide` (query.in_clause, still_scans), llms.txt, the
grange-query skill and the README.

v0.12.1

Toggle v0.12.1's commit message
M48: a mistyped route should fail, not answer

req.path carries the query string, so the obvious match was
has_prefix(req.path, "/count") — and that also matched /counter and /countXYZ,
which returned a perfectly plausible count. For an agent-first API that is worse
than an error: it teaches a wrong URL by rewarding it.

The inconsistency is what hid it. /health already matched exactly, so /healthzzz
404'd while /countXYZ did not, and the codebase looked like it had a policy. One
helper (srv_route) now expresses that policy once and all 24 route matches go
through it.

srv_route returns a BOOL, not 0/1: every call site uses it directly as a
condition the way has_prefix did, and the first version's int return was rejected
by the type checker at the call sites — the compiler being more useful than a
comment would have been.

Verified: /count, /count?coll=c, /ready, /verify?coll=c all work; /counter,
/countXYZ, /findx, /readyx, /usagex and six more all 404. make routes probes
eleven mistyped routes plus four real ones; reverting one route to the loose
match makes it report GHOST /counter answered instead of 404.

FOUND BY A CONTROL THAT REFUSED TO FAIL: the M47 journey harness renamed a
documented route to /counter to prove it could detect a contract advertising
something nonexistent, and it passed — because /counter genuinely answered.

Guide documents the matching rule (routes_are_exact).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

v0.12.0

Toggle v0.12.0's commit message
M46: liveness is not readiness

grange was already monitored — perrus polls /health on the primary and replica
every 60s with Telegram configured. That check answers "up" as soon as the accept
loop runs, and stays green through a read-only data directory, recovery having
dropped chunks, RSS about to trip the watchdog, or the nightly backup having
stopped weeks ago. It would not have noticed the M45 gap.

GET /ready answers those, returns 503 when any holds, and lists what is failing.
It reads no pages and runs no verify — a readiness probe that costs real work is
what eventually takes the server down.

Deliberate: /health stays trivial (two endpoints, two purposes); a missing backup
marker reports null rather than failing, because plenty of deployments have no
backup job and a false alarm teaches people to ignore the endpoint; and /ready
needs a token, since it reports RSS, version, collection count and backup age —
operational detail, not something to publish on a paid multi-tenant service.

scripts/readycheck.sh runs from a timer and messages Telegram ONLY on
transitions, reporting recovery too: a message every 15 minutes while something
is wrong gets muted, which is worse than no alert. It distinguishes `down` (no
response) from `failing` (responding, sick) — conflating them sends a misleading
page. It also reported `alerted: true` on the first observation when it
deliberately sends nothing; it now reports `transition` and `notified`
separately.

backup.sh records a success marker, which is what makes staleness detectable at
all, and the backup harness asserts it.

Verified on dk1: the first timer run caught a restarting server as failing, the
next saw ok, and the transition sent a recovery notice. /ready reports
backup_age_hours 0 against the nightly job; the replica reports role follower.

Guide gains `readiness` and GRANGE_BACKUP_MAX_HOURS; grange-operate gains a
section. routes_test probes /ready.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

v0.11.1

Toggle v0.11.1's commit message
M45: backups that exist

The v0.9.0 release notes said "Nightly backups". There were none — no timer, no
cron, no backup directory on the host. The claim predated the mechanism and never
became true. Corrected in place, like the SDK claim in M44.

scripts/backup.sh copies, verifies every collection with `grange verify`, and
prunes on a retention window — in that order. A plain copy IS a valid backup
here, and that follows from the format: .grg files are written 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.

A corrupt source exits 92 and the failed copy is KEPT, not pruned — deleting the
evidence of a failed backup is how you learn about it much later. Pruning only
happens after a good backup exists.

Tenant databases live in a sibling <db>.tenants directory, so `cp -a $DB` omits
every paying customer; both are taken. My first layout put the tenant roots
BESIDE the stamped directory, which made one backup look like two retention
entries and would have pruned half a backup — the harness now asserts one backup
is one entry.

scripts/backup_test.sh + `make backup` runs a backup under CONTINUOUS writes and
asserts: it verifies clean, the restore holds a committed prefix (never more than
the source), every restored document is whole, the restore is writable, tenant
data is included, a corrupt source exits 92, and the failed copy is kept.
Measured: source 165, restored 162.

Deployed: grange-backup.timer at 03:30 UTC, Persistent=true, keep 7. Verified
against production — the restored mirror counted 1160 against the live 1160, and
verify reported intact.

Guide gains a `backup` section and grange-operate a Backups section, per the
standing rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

v0.11.0

Toggle v0.11.0's commit message
M43: field projection — ask for the fields you need

A caller could 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 is the expensive gap.

?fields=a,b / --fields, honoured on find and export, on hot and cold, composing
with ordering and cursors. Measured on the live analytics mirror (11 fields per
event): asking for the 3 a dashboard needs took 5 rows from 1255 to 439 bytes,
65% smaller.

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 requested field the
document lacks is OMITTED rather than null (grange does not invent structure,
and machin's parse() treats missing and null differently); a dotted path
projects under its full path (user.id) FLAT, because rebuilding nested objects
is ambiguous as soon as two paths share a prefix.

Projection happens at emit time through one helper, so every plan goes through
it — hot scan, cold page walk, index resolution, ordered walk, export.

scripts/projection_test.sh + `make projection` compares projected against full
answers reduced in python, across both storage modes and five plans, plus: id
always present, missing field omitted, dotted paths, projected pages walking
every row exactly once, and that the payload actually halves. Negative control
verified — making single-field projection silently return full documents fails
four of the comparisons.

All four SDKs take `fields`. Guide + skills updated in the same change, per the
standing rule.

The embed harness earned itself again: query.src now calls proj_doc, so the
embeddable core stopped compiling and `make embed` caught it before poche did.
project.src added to both lists; poche rebuilt and green.

Gate: 6 suites, projection, guide, embed, replicas, soak, routes, pagination,
isolation, durability, caps, cold fuzz — green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>