Tags: javimosch/grange
Tags
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.
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.
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.
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.
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.
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.
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>
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>
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>
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>
PreviousNext