docs: correct every behavioural claim measurement contradicted #56
No reviewers
Labels
No labels
area:defold
area:docs
area:infra
area:protocol
area:testing
kind:bug
kind:chore
phase:breadth
phase:core
phase:hardening
phase:slice
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
yuris/spacetimedb-defold!56
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/doc-truth"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The last item on #24's release gate: every falsifiable behavioural claim in
README.md, verified by execution against a live SpacetimeDB 2.8.2. ~155 claims exercised by 268 Lua assertions and ~45 live wire probes, treating the README as a hypothesis rather than documentation.30 of 30 in-scope findings actioned, plus 2 extras. Four were already fixed by #53 and correctly skipped.
First, what held
The parts most likely to have rotted did not. All the subscription quirks are true byte-for-byte — including the #43 overlap shape, the identical-text collapse, and the legacy
Subscribebehaviour. All the procedure quirks are true, and a third failure case was found that strengthens them (bad arguments also giveInternalError). The whole special-type table is true, including the 128-bit decimal-string contract, the micros units,u128 MAX, and every numeric bound: 2^53 exactly, whole-second resolution across 4,527,877 sampled micros with zero mismatches.So this is surgical correction, not a rewrite.
The most important fix: the README was talking itself down
It said the other direction of light mode "is still unproven against a server — that another client's transaction really does arrive light — which needs a second connection the harness does not have."
It is proven. Two sockets on one server: the
light = trueclient received the other client's transaction asTransactionUpdateLightwith keys exactly["request_id","update"], while the ordinary socket got the fullTransactionUpdatewithreducer_callfor the same commit.A false disclaimer is worse than a plain error, because a reader believes it and designs around a limitation that does not exist.
The same section understated a real hazard, now sharpened by measurement:
TransactionUpdateLightdoes carry a top-levelrequest_id, and it is the originating client's. A probe sent4242from the heavy socket while the light socket's own last id was7, and4242is what the light frame carried. That is worse than carrying none — a correlator reading it matches another player's call.Claims that were false
session.jsonlis four blocks, not three. #51 appendedoverlap;IdentityTokensits at frames 1, 30, 37, 43. The per-block example listed only two./48with the overlap block.[html5] SMOKE: PASS procedure round trip…was spliced from two real lines. Corrected to the realPHASE4 OK …text, with the insert count taken from an actual conformance run rather than invented. The PHASE5SMOKE: PASSline was deliberately not quoted: its text ends in a run-specific identity prefix, so quoting one would have replaced a fabricated line with another. All four other quoted log lines were checked and are real.Option<Point>is[0,{"x":8,"y":9}]named and[0,[8,9]]positional. Same for theNoneshape:[1,{}]vs[1,[]].u128/ConnectionId/Uuid— the description was weaker than the truth.u128arrives a bare rounded number;ConnectionIdandUuidarrive as a one-element array holding one, and the helpers return nil for them.updatescounted per distinct query — actually counted per pool, and the newSubscribeSinglefact defines what a pool is.dev/nix/spacetime.nix:6is fixed too — outside the README-only scope, so flagged rather than done silently.Claims that were unfalsifiable as written
https://user:token@…/archive/v0.1.0.zipreturns 200,?token=…returns 401, and a nonexistent repo returns 404 with and without basic auth. The false mechanism is gone; the actionable advice stays.unknown"when and only when nothing matched." The only when half is true — seven exotic types including a zero-variant sum and a triple-nested tag all resolved. The when half is unreachable: every anonymising route is rejected at publish.OutOfEnergycould not be constructed on a self-hosted standalone. Marked documented-but-unobserved rather than deleted, in both places a reader might land.Six behaviours nobody had written down
request_idis required, not an optimisation — the README said this only forone_off_query.SubscribeMultiAppliedfor a query matching nothing omits the relation; a legacySubscribematching nothing carries it withnum_rows: 0.SubscribeSingleshares the query pool withSubscribeMulti— only the legacySubscribeis a separate pool.OneOffQuery.message_idgoes out as a byte array and comes back as a lowercase hex string ([1,1,1,1]→"01010101"). Neither direction was documented.__identity__keeps its own name —product_FakeIdentity, notidentity. Extends the shape-collision rule fromOptionto the special types.tablesis accepted by 2.8.2, which makesdb.tables.tablesa real case rather than a hypothetical.Two places the verification agent's own findings were wrong
Both caught by re-running rather than re-reading.
Procedure ordering. The claim was that the
InternalErrorreplies "overtake the two that do work". But reply 3 — a real throw, also attimestamp: 0— landed last, behind working reply 2. Written as the weaker, true statement: a failure is answered without a transaction, so nothing holds it behind the calls that do work, and it can land ahead of one sent before it..rows.table_rows"a bareTableUpdatewith no array" — it does carry its ownupdatesarray; only thetablesarray is absent.Verified beyond what was asked
The
decode_typedexample was broken: it passedgen.tables.playerthen readrow.status/row.home/row.joined/row.serial, but this repo's generatedplayeris{ identity, sponsor, name, score }— a reader following it got four nils. Now usesgen.tables.all_typesand every value in it is measured:status → { tag = "busy" },point → { x = 3, y = -4 },at → 1787163313278160, and the exact 39-digitu128, identical in both encodings.#55 had meanwhile renamed the namespace illustration to a table called
cache, which nobody had tested. A module declaring bothcacheandtableswas published and both served rows — so #55's renamed line is now verified too.The
"second",2view literal was softened and explained: the module returnsrows[rows.length-1], and 2.8.2's iteration order is not insertion order, so with both rows present the run returned the first inserted one."Neighbouring integers share a double" was graded rather than left universal: 0% collide below 2^53, half the adjacent pairs just past it, ~all near
u64max. All three stated consequences hold and stay.Verification
6 passed, 0 failed; stylua clean. Native conformance PASS both runs through PHASE5 with identity persisted across a process restart — the PHASE4 line now quoted in the README came out of that run verbatim. No fixture drift; the schema was re-derived from the published module and diffed through stylua.Every quoted claim was located by its text rather than by line number, since #55 moved them. Nothing was unlocatable.
Every claim here was executed against a live 2.8.2, not re-read. 14 were false, 5 were unfalsifiable as written, 5 misleading, and 6 real behaviours were undocumented. The one that matters most: the README said light mode's other direction "is still unproven against a server ... which needs a second connection the harness does not have". It is proven. Two sockets on one server: the `light = true` client received the OTHER client's transaction as `TransactionUpdateLight` with keys exactly `["request_id","update"]`, while an ordinary client got the full `TransactionUpdate` for the same commit. A disclaimer is worse than a plain error, because a reader believes it and stops looking. The same paragraph gained the sharper hazard underneath it: a light update DOES carry a top-level `request_id`, and it is the ORIGINATING client's, so a correlator reading it matches another player's call against its own pending table (measured: id 4242 from the other socket, while this one's last id was 7). Other corrections, each with the measurement behind it: - `session.jsonl` is four blocks, not three: `IdentityToken` at frames 1, 30, 37, 43, and `capture-fixtures.mjs` declares `BLOCKS = { player, views, overlap }`. The per-block example gained `overlap` too. - Two quoted outputs could not be produced. The fixture FAIL message gained `/48` with the overlap block, and the smoke line spliced `SMOKE: PASS` onto PHASE4's text; the only PASS the script emits is PHASE5's verdict. Both now read what the source prints. - The HTML5 phase list named four phases and omitted the two carrying the verdict: phase 1's `light = true` assertion and PHASE5's identity-keyed table. - `sub.message` retains 0.4 MB for a 4000-row set, not "roughly 2 MB"; 2 MB needs ~470 bytes a row. - An `Option` is NOT the same shape in both encodings once its payload is a product, and a positional `None` is `[1, []]`, not the `[1, {}]` a named readback carries. - `updates[]` counts per pool, and `SubscribeSingle` shares the pool with `SubscribeMulti`; only the legacy `Subscribe` is separate. - `decode_typed`'s example read four columns `gen.tables.player` does not have. Rewritten on `all_types` and executed against the captured frames. - Positional `ConnectionId`/`Uuid` were a one-element array, not "a rounded number", and the helpers returned nil for them. - "Neighbouring integers share a double" was graded: 0% below 2^53, half just past it, effectively all near `u64` max. - The conformance diff normalises through stylua first, so "ignoring only line 1" was false. - Forgejo's archive endpoint does not 404 on basic auth — it serves the archive. Dropped the mechanism, kept the advice. - `OutOfEnergy` and the `confirmed_reads` rationale are now marked as what they are: declared by the wire, or unobservable, rather than captured. - `unknown` is unreachable from any 2.8.2 JS module, and the shape-collision rule extends to the special types (`product_FakeIdentity`, not `identity`). - Stale nixpkgs figure fixed in README and `dev/nix/spacetime.nix`. Newly documented, all consumer-visible: procedure replies arrive out of request order (sent 1,2,3,4,5, got 1,4,5,2,3); "nothing came back" has two shapes and table state picks one, not the message type; `OneOffQuery.message_id` goes out a byte array and comes back a hex string; and 2.8.2 accepts tables called `cache` and `tables`, so the namespace collision is real rather than hypothetical. One correction is to DocTruth's own finding. It reported the empty-result shape as depending on the message type, from two probes run against different table states. Isolating the variable shows both message types agree on every cell and the table's own emptiness is what decides it. Suite 6/6, stylua clean, native conformance PASS through PHASE5, no fixture drift.Review — automated reviewer, posted for the record
The reviewer died before yielding, but wrote findings to disk as instructed. This is that file, verbatim, salvaged in full. Its probes are under
/tmp/r56/.Headline: the PR's flagship correction is itself false.
light=trueis a no-op on 2.8.2 for the behaviour claimed — the discriminator is caller-vs-non-caller. Proven with a controlled 4x4 matrix. Being actioned now.Review of PR #56 (feat/doc-truth) — Review56
Base
4ee2a24, headbb00355. Started.Setup
.#cishell lacks openssl; dev-server.sh needs it on a fresh state dir -> must use.#conformance. (Observation, not a PR defect.)TASK 1 — headline light-mode correction: PARTLY FALSE (candidate BLOCKER)
Probe
/tmp/r56/p1-light.mjs, own harness/tmp/r56/w.mjs, server 6500.Three sockets, all
SubscribeMulti 'SELECT * FROM message':light=?light=trueheavy= ordinary, and the CALLERplain= ordinary, NOT the callerHeavy calls
send_messagewith request_id=987654.Observed:
Light client's own call -> full TransactionUpdate w/ reducer_call.request_id=7. OK.
So the README's new sentence "...while an ordinary client gets the full
TransactionUpdatefor the same commit" is not reproducible: the ordinary NON-CALLER got the light frame too.
The discriminator looks like caller-vs-not, not light-vs-ordinary. Needs the controlled run.
TASK 1b — controlled run:
lightis a NO-OP on 2.8.2 for this behaviour/tmp/r56/p2-light-controlled.mjs. Four observer sockets (ordinary/light x our-token/other-identity)plus a caller varied four ways:
| caller | ord-sameToken | lit-sameToken | ord-otherIdent | lit-otherIdent |
| HTTP /call (no socket) | Light rid=0 | Light rid=0 | Light rid=0 | Light rid=0 |
| separate ordinary sock | Light rid=555 | Light rid=555 | Light rid=555 | Light rid=555 |
| ord-sameToken itself | FULL | Light rid=111 | Light rid=111 | Light rid=111 |
| lit-sameToken itself | Light rid=222 | FULL | Light rid=222 | Light rid=222 |
=> The discriminator is CALLER-vs-NON-CALLER, not light-vs-ordinary.
light=truechanged nothing observable. So README:1131-1132 "while an ordinary client gets the
full
TransactionUpdatefor the same commit" is FALSE as written.TASK 1c — the flag IS parsed, so the no-op is real
/tmp/r56/p3-lightflag.mjs:?light=true/falseopen;?light=banana/?light=1-> HTTP 400"Failed to deserialize query string: provided string was not
trueorfalse".So the server does read
light; it simply does not change which frame a NON-CALLER gets.TASK 1d — SDK comment now contradicts the README (pre-existing, but the PR makes it visible)
stdb/connection.lua:824-825: "Light frames are other clients' transactions, and thosereport
request_id = 0." Measured: light frames carry the ORIGINATOR's request_id(555 / 111 / 222); only the HTTP-called commit reported 0 (HTTP has no request_id).
README:1124-1127 (new, correct) and connection.lua:825 (stale, wrong) now disagree.
TASK 1e — NEW FALSE CLAIM added by this PR: the
confirmed=banana400 bodyREADME:1141-1143 "…
?confirmed=bananaanswering400and naming the string that was neither."Exact body (77 bytes, curl against 6500):
Failed to deserialize query string: provided string was not
trueorfalseIt names the two ACCEPTED values, not
banana. It does not even name which parameterwas bad:
?light=bananareturns the byte-identical body.TASK 2a — Option shape per encoding: HOLDS (measured)
/tmp/r56/p4-optshape.mjson 6500,all_types.opt_point=Option<Point>, same server, same table,positional row from the TransactionUpdate, named row from a SubscribeMultiApplied readback.
| column | POSITIONAL | NAMED |
| opt_point Some |
[0,[8,9]]|[0,{"x":8,"y":9}]|| opt_point None |
[1,[]]|[1,{}]|| opt_int Some |
[0,7]|[0,7]| (scalar identical, as claimed)| opt_int None |
[1,[]]|[1,{}]|=> README:40-43 and README:767-773 (
Option<Point>is[0,{"x":8,"y":9}]named and[0,[8,9]]positional;
None[1,{}]vs[1,[]]) are exactly right.TASK 2b — positional u128 / ConnectionId / Uuid: HOLDS on the wire
Same run:
u_128 = 3.402823669209385e+38 (bare rounded number) <- as claimed
conn = [7.630495884497377e+37] (one-element array) <- as claimed
uid = [1.512366075204171e+36] (one-element array) <- as claimed
Named readback for the same row: conn
{"__connection_id__":…}, uid{"__uuid__":…}.Incidental:
owner(identity)["0x…"],at(timestamp)[1787216177942060],span[90061000000]are one-element arrays positionally too, and
u_256positional is the hex STRING"0xffff…ffff", not a number. The README's list is about which ones LOSE PRECISION, sonaming only u128/ConnectionId/Uuid is defensible, but the shape rule is broader.
TASK 2b(ii) — "the helpers refused outright": HOLDS in practice, mechanism mis-stated (nit)
/tmp/r56/h.lua(pure Lua 5.1, repostdb.protocol):connection_id_to_hex({12345}) -> 0x…3039 (accepted)
connection_id_to_hex({2^53}) -> 0x…20000… (accepted)
connection_id_to_hex({2^53+2}) -> nil
connection_id_to_hex({7.630495884497377e37}) -> nil (a real positional conn)
uuid_to_string({1.512366075204171e36}) -> nil (a real positional uuid)
binary search: last accepted 9007199254740992 (= 2^53), first refused 9007199254740994.
scalar()unwraps the positional[n]fine (protocol.lua:200-218); the nil comes fromu128_digits' 2^53 gate (protocol.lua:350). So the refusal is PRECISION-driven, notshape-driven — but a server-minted 128-bit id is below 2^53 with probability 2^-75, so
README:947-949 "which the helpers refused outright" is true to any practical standard.
Nit only: the sentence attributes the nil to the array wrapper.
TASK 3 — the six new behaviours
(1) procedure replies out of request order: HOLDS, and the weakened wording is the true one
/tmp/r56/p5-six.mjs. Sent request_ids 1..5 = count_messages, always_fails, echo,no_such_procedure, echo(bad args). Arrival order 1,4,5,3,2.
rid=1 Returned ts=1787216345372446 dur=15752
rid=4 InternalError ts=0 dur=0
rid=5 InternalError ts=0 dur=0
rid=3 Returned ts=1787216345399082 dur=406
rid=2 InternalError ts=0 dur=0 <- the real throw, LAST, behind working reply 3
=> DocTruth's "failures overtake the calls that do work" is refuted again, independently.
=> README:534-540's weaker wording ("nothing holds it behind the calls that do real work,
and it can land ahead of one sent before it") is exactly what I measured: 4 and 5 landed
ahead of 2 and 3, both sent earlier. Does not under-warn: it still says correlate on
request_id, "the only thing that pairs a reply with its call".
NIT: the quoted permutation "came back 1, 4, 5, 2, 3" is run-specific — my run gave
1,4,5,3,2. Past tense makes it an honest observation, not a fabrication, but it is a
run-specific literal in a document that quotes literals as facts.
(4) OneOffQuery.message_id byte array out / hex string back: HOLDS
Sent
[1,1,1,1]->"01010101"; sent[222,15,0,255]->"de0f00ff". Lowercase hex string.(3) SubscribeSingle shares the query pool: HOLDS for the delta count, but the
PR's sentence over-generalises the pool's three consequences (should-fix)
/tmp/r56/p6-pools.mjs, all four sets using the IDENTICAL textSELECT * FROM message:A1 Multi(100) -> SubscribeMultiApplied WITH the row
A2 Multi(101) identical ->
update:{"tables":[]}(replies with nothing) OKA3 one change -> 1 updates entry OK
B1 Single(102) identical -> SubscribeApplied WITH the row, num_rows=1 <- NOT "nothing"
B2 one change (2 Multi + 1 Single) -> 1 updates entry OK (shares pool)
C1 legacy Subscribe -> InitialSubscription WITH the row
C2 one change (+legacy) -> 2 updates entries OK (separate pool)
D1/D2 unsub both Multi ->
update:{"tables":[]}each (deletes nothing) OKD3 one change (Single+legacy) -> 2 entries OK
E1 unsub the Single -> UnsubscribeApplied WITH the DELETE OK (last to go)
E2 one change (legacy) -> 1 entry
=> "The single-query form shares the query-set pool" is TRUE (delta count 1 with
Multi+Multi+Single). But README:346-352 then states the pool's sharing consequences
flatly — "Inside a pool, sharing means the second subscribe replies with nothing" —
and that is FALSE when the second subscribe is a
SubscribeSingle: it replied with thefull relation. Fix: scope "replies with nothing" to a second query set
(
SubscribeMulti), and say that aSubscribeSinglejoining the pool still gets its ownreadback while counting as the same query in
updates[].(2) the two empty-result shapes: rule HOLDS for Multi+legacy; the bullet's HEADLINE is
falsified by SubscribeSingle (should-fix)
/tmp/r56/p7-empty.mjs+/tmp/r56/p8-stopped.mjs:| state | SubscribeMulti | legacy Subscribe | SubscribeSingle |
| table EMPTY, unfiltered | ABSENT | ABSENT | PRESENT num_rows=0 |
| table HAS rows, query matches none | PRESENT n=0 | PRESENT n=0 | PRESENT n=0 |
| drop one of two identical-text sets (released 0) | ABSENT | - | - |
| drop a query that had STOPPED matching | PRESENT n=0 | - | - |
| drop the last holder | PRESENT, real delete | - | - |
=> The four-way rule in README:363-376 is exactly right, and "Both a
SubscribeMultiand alegacy
Subscribebehave identically on each" is exactly what I measured.=> BUT the bullet's bold headline — "and the message type is not what picks one" — is
false for the third form:
SubscribeApplied.rowsis a REQUIRED single TableUpdate(protocol.lua:58 lists
rowsas required; notablesarray to be empty), so aSubscribeSinglecan NEVER report the relation absent. On an empty table it returnedPRESENT num_rows=0 where both other forms returned ABSENT.
This is compounded by the PR's own new "SubscribeSingle shares the query-set pool" line:
a reader is now told Single behaves like Multi, and on this shape it does not.
Fix: say "neither
SubscribeMultinor the legacySubscribepicks a shape — the statedoes;
SubscribeSingleis the exception, it always names the relation because itsrowsfield is not an array."extra fix verified —
.rows.table_rowsDOES carryupdatesSubscribeAppliedraw:"rows":{"table_id":4097,"table_name":"message","table_rows": {"table_id":4097,"table_name":"message","num_rows":1,"updates":[…]}}.FixDocs' correction (README:365-367) is right, and DocTruth's "bare TableUpdate with no
array" was wrong. HOLDS.
converse pool order (extra control)
Single(10) FIRST -> PRESENT with the row; Multi(11) identical text SECOND -> ABSENT;
Single(12) identical text THIRD -> PRESENT with the row again.
So the "second subscribe replies with nothing" behaviour belongs to the MULTI reply path,
not to the pool: a
SubscribeSinglealways gets its readback. Confirms the fix above.(5) named product declaring
__identity__keeps its own name: HOLDSPublished my own module to db
r56clashon 6500 (/tmp/r56/mod/index.js), thengen-schema.sh http://127.0.0.1:6500 r56clash:faker.types = { "u64", "product_FakeIdentity", "product_FakeTimestamp" }
typespace ["FakeIdentity"] = { kind="product", fields={"identity"}, types={"u256"} }
["FakeTimestamp"] = { kind="product", fields={"timestamp…_"}, types={"i64"} }
Stronger than the README says, and worth it: on the WIRE the fake is byte-identical to a
real identity — named
{"__identity__":"0x7"}, positional["0x7"]— so the schema tag isthe ONLY discriminator. Same for the timestamp magic name.
(6) a table called
tablesis accepted: HOLDSSame module declared
tablesANDcache; publish succeeded and both served rows:served table_name=tables rows=1 inserts=["[1,"tables:hi"]"]
served table_name=cache rows=1 inserts=["[1,"cache:hi"]"]
So
db.tables.tablesanddb.tables.cacheare both reachable cases. README:314-322 holds.TASK 5 — the four skips
SKIP 1 (Rank 10,
include_dirs/ "all a consuming project sees"): genuinely fixedREADME:189-191 now reads "The archive also carries
README.md,game.projectandpatches/, butinclude_dirs = stdbis what Defold mounts, sorequire("stdb.init")isthe only thing on your require path." — verbatim DocTruth's replacement. Skip is correct.
SKIP 3 (Rank 7,
stdb.url): genuinely fixedREADME:1093
| stdb.url(base_url, database) | string | <base_url>/v1/database/<db>/subscribe.base_urlmust carry the scheme —ws://127.0.0.1:3000— because nothing here adds one |.Skip is correct.
SKIP 2 (Rank 14, archive sizes): the skip was correct WHEN READ, and THIS PR broke it
-> should-fix, and it is this PR's own doing
README.md:157-158 "takes the zip a consumer fetches from 54 entries and 220 KB down to
11 entries and 86 KB".
Measured with
git archive --format=zip:at #53 (
65282fb) release zip = 87,751 B = 85.7 KiB -> "86 KB" OKat base
4ee2a24release zip = 88,796 B = 86.7 KiB -> "86 KB" OKat PR head
bb00355release zip = 90,717 B = 88.6 KiB -> "88 KB" STALEpre-export-ignore at head: 54 entries, 228,254 B = 222 KiB ("220 KB" -> 222 KB)
entry counts 54 / 11 both still exact.
Cause: this PR grows README.md from 79,760 B to 84,771 B (+5,011 B, +70 lines), and
README.md is the single largest member of the release archive. A PR that exists to make
the README's numbers true silently invalidated two of them.
Fix: 222 KB and 88 KB, or drop the KB figures and keep the entry counts (which are stable).
SKIP 2 knock-on —
.gitattributes:1-11header comment is stale AND self-refutingIt claims: "54 entries and ~220 KB of zip before, 11 entries and 87,646 B after. Of the
232,696 B that survives, README.md is 76,502 B (33%) and stdb/ is 153,302 B. The 'after'
figures are exact and stay exact; this file is itself export-ignored, so editing this
comment cannot change them."
Measured at PR head: zip 90,717 B (not 87,646); uncompressed total 241,025 B (not 232,696);
README.md 84,771 B = 35% (not 76,502 B / 33%).
Even at
65282fb— the commit that wrote the comment — the zip was 87,751 B, not 87,646 B."stay exact … editing this comment cannot change them" is the false premise: the figures
depend on README.md, which is IN the archive and changes constantly. DocTruth's install
slice flagged this file by name (
/tmp/doctruth-install.md:338); FixDocs skipped it as"already fixed by FixArchive", and it is not fixed.
SKIP: install slice "four things" nit — correctly rejected
README:176 already reads "Four things cannot move into
dev/, each because a tool demandsthe root:". Rejection reason accurate.
SKIP: bounds item 4 (decode_typed ratio content-dependence) — correctly rejected
/tmp/doctruth-bounds.md:322-327— its own author says it belongs inprotocol.decode_typed's docstring, not the README, and that the README's as-written~2x needs no change (1.95x-2.07x on the real row). Rejection accurate.
SKIP: install-URL
v1.0.0pin — correctly rejectedREADME:185,211 point at
archive/v1.0.0.zip, which is the tag this release creates.A precondition of the release order, not an error.
TASK 5 extras
EXTRA 1 —
dev/nix/spacetime.nix:6version: VERIFIED, and it belongs herenixpkgs master
pkgs/by-name/sp/spacetimedb/package.nix->version = "2.8.1".nixpkgs
nixos-26.05->version = "2.2.0".license = lib.licenses.bsl11.So both README:1408-1409 and spacetime.nix:6 are now right (2.2.0 / 2.8.1 / 2.8.2 here).
Belongs in this PR: comment-only one-liner, identical stale figure, disclosed in the PR
body, and splitting it would leave the file that PINS the version contradicting the README
about that version. Note
dev/is export-ignored, so it is not part of the shipped surface— which is exactly why it needs to ride along rather than get its own PR.
EXTRA 2 —
.rows.table_rowscorrection: VERIFIED (see TASK 3 above). Right call.Other new claims I attacked
unknownis unreachable from a 2.8.2 JS module: HOLDSPublished three modules to 6500 (
/tmp/r56/anon/):anonymous
t.object({a})-> publish REJECTED (SchemaInner throw, 500)t.object('', {a})-> "module contains invalid identifier: Empty identifiers are forbidden."anonymous
t.enum(['A','B'])-> publish REJECTEDREADME:708-712 ("a compound type needs a name — an anonymous one is rejected at publish, an
empty one by the identifier check") is exactly right, including which check catches which.
TASK 7 (partial)
lua dev/test/runner.lua-> 6 passed, 0 failedstylua --check stdb dev/test-> clean, exit 0published module"). Full run in progress.
TASK 3 — PLACEMENT of the six new behaviours
| behaviour | landed at | section | would a consumer look there? |
| procedure reply ordering | README:534-540 |
### Procedures, 3rd of "Three things to know" | YES — the count was correctly bumped from "Two" to "Three" || empty-result shapes | README:369-377 |
### Dropping a subscription, 4th bullet | PARTLY. Half the rule is about SUBSCRIBING (a narrowed subscribe matching nothing); a reader whose subscribe came back empty would not look under "Dropping a subscription". It is still the least-bad home, since the other three delta behaviours live there. || SubscribeSingle pool | README:348-349 and README:616-619 | both
### Dropping a subscriptionand### Reconnect'supdatesparagraph | YES — both places a reader meets the delta count || OneOffQuery.message_id | README:588-592 | inside
### Reconnect, right after theevt.matchedparagraph | YES, that is where the existingmessage_idcorrelation material already is ||
__identity__named product | README:724-726 |### Generating the schema, appended to the structural-recognition paragraph | YES || a table called
tables| README:317-322 |### Per-table callbacks, in thedb.tablesnamespace rationale | YES — it is the paragraph that motivates the namespace |None is in a footnote. Verdict: 5 of 6 land exactly where a consumer hits them; the
empty-result one is defensible but its own text spans subscribe as well as drop.
PLACEMENT knock-on: README:342 "Four behaviours are worth knowing" — the count is now
ambiguous, and the PR bumped the analogous count elsewhere but not this one (should-fix)
Bullet counts under that sentence, measured with
git show <c>:README.md:13073c6(#51, which WROTE "Four behaviours") 3 bullets65282fb(#53) 3 ·4ee2a24(#55, base) 3 ·bb00355(this PR) 4So either bullets are the behaviours — in which case "Four" was wrong before and is right by
accident now — or the trailing "Holds are counted per primary key" paragraph was the fourth,
in which case "Four" is now wrong at five. The PR correctly changed "Two things to know" to
"Three things to know" for Procedures (README:527) after adding one bullet there, and did
not do the same here. Fix: state the number that matches, or drop the number.
TASK 4 — quoted literals I checked myself
["second",2]and the REASON given for softening it — reason NOT reproducible (should-fix)/tmp/r56/p10-views.mjs,/tmp/r56/p11-iter.mjs.README:853-859 now says: "Which row that is belongs to the module and the server, not to
this SDK: the view here returns
rows[rows.length-1]and 2.8.2's iteration order is notinsertion order."
First half VERIFIED:
dev/server/index.js:182-186really returnsrows[rows.length - 1]and
undefinedwhen empty.Second half NOT REPRODUCIBLE. Nine observations on my server:
clean table, rows "first"(1) "second"(2), three consecutive looks ->
["second",2]all three6 fresh inserts -> message_flip iteration order [3,4,5,6,7,8], latest = ["w6",8]
after clear + 3 inserts-> [9,10,11], latest = ["x3",11]
after a pk-preserving edit -> [9,10,11], latest = ["x3",11]
Iteration order was pk-ascending — i.e. insertion order for an autoinc pk — every time, and
message_latestreturned the LAST inserted row every time. On a clean table["second",2]is in fact deterministic, which makes the added clause both unsupported and self-defeating:
it explains the softening with a mechanism that does not fire.
Fix: keep "such as
["second",2]" and keep "which row belongs to the module", drop orattribute the iteration-order clause (e.g. "the module picks
rows[rows.length-1], andnothing in the protocol promises which row
iter()yields last").the
nonecase carries no relation at all: HOLDSEmpty table ->
"update":{"tables":[]}— nomessage_latestentry. README:857-859 right.session.jsonlfour blocks: HOLDS50 frames;
IdentityTokenat lines 1, 30, 37, 43;capture-fixtures.mjs:235const BLOCKS = { player: playerSteps, views: viewSteps, overlap: overlapSteps }-> main + 3.the
4/5/24/48FAIL line: label and format both HOLDdev/test/test_fixtures.lua:175label = "generated column order matches the module, whichdecodes the TransactionUpdate rows in frames 4/5/24/48";
dev/test/runner.lua:26formatsFAIL %s: expected %s, got %s. The quoted line is producible.RUN-SPECIFIC value quoted as general:
row id=1(should-fix)README:129-133's fenced block quotes
[html5] SMOKE: PHASE2 OK survived restart; row id=1 restoreddev/example/main/smoke.script:240formats it fromself.edited_id, a server autoinc u64.My own conformance run printed
row id=1in run 1 androw id=4097in run 2 of the sameinvocation. So the id is run-state-dependent and is quoted as a bare fact — the exact bar the
author says they applied when they declined to quote the PHASE5 identity-prefix line.
The line is pre-existing, not added by this PR, but this PR is the audit of that block and
it edited the line directly beneath it.
Fix:
row id=<n>/row id=1 restored->row id=… restored, or drop the id.TASK 7 (cont.) — native conformance PASS
STDB_PORT=6500 STDB_STATE_DIR=/tmp/r56/state dev/tools/conformance.sh:SMOKE: PASS PHASE5 …, identity 0xc2007bcf…PASS (identity … persisted across a process restart)[html5]prefix, which is the HTML5 tier's own —
html5-conformance.mjs:165[html5] ${line}):SMOKE: PHASE4 OK procedure round trip; failure surfaced as an error; per-table API: 1 insert(s), 1 update