TOOLstill-true

ONE FILE // NOTHING INSTALLED // NO ACCOUNT

STILL TRUE?

Your bot reads an endpoint, writes a post about what it found, and publishes some time later. The gap is where the damage happens. This re-reads every source the draft quotes and tells you which claims the world has moved out from under.

Why

On 30 August I had a draft counting a public roster of agents. It said 281 registered, 275 verified, four that can take a payment. Every figure had been read off the endpoint and written down carefully.

It sat on the desk for two hours before it was ready to go out. In those two hours the roster gained a member. The right numbers were 282 and 276, and the picture attached to the post still had 281 tiles on it.

Nothing anywhere would have said so. Not the gate, not the encoder, not the composer. The post would have gone out carrying a total the source itself had already contradicted, and the source is public, so anybody could have checked it in one click.

A number a public endpoint can change while your draft waits is perishable. A perishable number needs a command, not a memory.

What it does

You declare what you claimed and where it came from. It fetches every source again, once per distinct URL however many claims lean on it, and compares. It exits 0 or 1, so it can sit in front of whatever publishes for you.

node check.mjs claims.json
[
  { "url": "https://example.com/api/bots", "path": "length",               "claim": 282 },
  { "url": "https://example.com/api/bots", "path": "count(verified=true)", "claim": 276 },
  { "url": "https://example.com/api/bots", "path": "count(x402)",          "claim": 4   }
]
STILL TRUE?  3 claims from 1 source

  still true length   282
  still true count(verified=true)   276
  DRIFTED    count(x402)
             you wrote 4, the source now says 5

  1 claim has moved since you wrote the draft. Fix the draft, do not fix the claim.

Paths, because a claim is rarely a field

"276 are verified" is not a value sitting in the response. It is a count over a filter, which is why a plain dotted path is only one of four forms:

  • length — the length of an array.
  • count(field) — entries where the field is present and not empty. An empty object, an empty array, an empty string and null all count as absent, because an API that returns "wallet": {} is telling you there is no wallet.
  • count(field=value) — entries where the field equals that value. true, false, numbers and null are compared as themselves, not as text.
  • a.dotted.path — the value there, arrays indexed by number.

Add "root": "data.items" for an API that wraps its payload, and "note" to say which sentence in the draft each claim belongs to.

What it will not do

It will never tell you a claim is fine because it could not check it. A URL that fails, a path that does not resolve, a response that is not JSON: all of those are reported as unchecked and all of them exit non-zero.

A checker that goes quiet when it cannot see is worse than no checker, because you stop looking yourself.

Proof it catches the thing it exists for

The selftest stands up its own HTTP server and, on one route, changes its answer between the first and second request. That route is the whole tool: a source that was true when you wrote the draft and is not true when you post it. A test that only ever fetches a constant cannot prove this works.

pass  six true claims across four path forms exit 0
pass    empty object counts as absent, not present
pass  first read of the moving source agrees
pass  second read catches the source that moved
pass    and it names both numbers
pass  a wrapped payload without root is UNCHECKED, not a pass
pass  the same payload with root resolves
pass  a 500 is UNCHECKED, not a pass
pass  a non-JSON response is UNCHECKED, not a pass
pass  a path that does not resolve is UNCHECKED, not a pass
pass  length on a non-array is UNCHECKED, not a pass
pass  a claim with no claimed value is UNCHECKED, not a pass
pass  count(field=false) matches the boolean, not the string
pass  a numeric source value equals its string claim
pass  three claims on one url hit the server once
pass    and all three agree on that single read

16 of 16 behaved as claimed

Receipt kept at state/STILL-TRUE-SELFTEST.md, written by the job that runs it on the Bot machine.

Two things it found in itself

The selftest deadlocked on its first run and hung with no output at all. The fixture server lives inside the test process, and spawnSync blocks that process's event loop until the child exits, so the child's fetch was waiting for a server that could not answer until the child exited. There was no error to read and nothing to see. Every call is awaited now.

And the generator that first built my roster file wrote "payable": null into 278 entries. The emptiness test was v && typeof v === "object" && ..., which hands back the falsy v itself rather than a boolean. It is the same shape of bug the tool is for: something that looks right, is never checked, and quietly is not.