api: eight renames the 1.0 freeze makes permanent #55

Merged
yuris merged 2 commits from feat/api-freeze into main 2026-08-20 08:38:42 +00:00
Owner

The API surface freeze review from #24's release gate, actioned. Eight renames, because the tag makes every name permanent and this is the last cheap moment. No behaviour changes except one silent-failure bug that the naming inconsistency was hiding.

A freeze review enumerated every public name — every module, function, parameter, event type string, table field and handle method — and counted every call site. Eight were worth the churn; one was rejected.

The eight

1. Event type = "error" → "socket_error" (+2/-1)

It was the only emitted event type absent from the README's event table — 16 rows for 17 types. An undocumented string literal in a frozen surface is the worst possible artifact: undiscoverable and unchangeable. It was also the most generic name on the surface given to the most specific case (one transport's error callback), sitting beside evt.error, sub.state == "error" and call.state == "error" — four unrelated meanings of one token. Every sibling socket event is a participle.

The table is now 17/17, enumerated by hand, including the three dynamically-built sites (connection.lua:764, :899, :934) that a literal grep cannot see.

2. db.state → db.cache (+122/-114)

The connection's cache was db.state, while the two handles a caller holds at the same moment spell their lifecycle sub.state / call.state:

if sub.state == "applied" then stdb.cache.get(db.state, ...) end

This is the exact defect this release already fixed once — connection.lua:264 says "Named state and not outcome to match Subscription.state: one concept deserves one name." The same rule condemned db.state. tables.lua:22 already confessed it in prose.

cache.lua's own first parameter was renamed too; leaving it would have kept the word in play. Verified zero cache-ish .state field accesses survive — every remaining one is sub/call lifecycle or a fixture row column literally named state. Test-local state/*state variables in test_cache.lua and test_fixtures.lua are left alone: they are not surface, and ~30 lines of test churn at a freeze buys nothing.

3. undescribed[].rows → dropped (+22/-27)

A count named as a plural noun. README:811-816 spent six lines apologising for it, and the apology was the proof: "The .row on an insert or delete event is the row itself; these are different fields." e.row (a row) and evt.rows (a number) differed by one character and by kind.

Moved in all three places at once — the event, counts.undescribed[i], sub.undescribed[i]. Both apologies deleted, including a five-line paragraph in connection.lua that the rename makes obsolete.

4. schedule_at_to_seconds returns "Interval"/"Time" (+22/-19)

This one was a bug, not a naming preference. decode_value yielded {tag="Interval"} while the helper returned "interval", and module-declared sums are server-lowercased ({tag="busy"}). So a consumer learned "tags are lowercase", wrote tag == "interval", and got false forever with nothing raised.

Fixed the helper, not the tag: Interval/Time are the server's own declared variant names (gen-schema.sh:210 matches them off the live schema endpoint), so the decoder was right and the lowercasing was this SDK's invention. protocol.lua:275-277's apology comment is deleted.

The reproduction is now a permanent guard: schedule_at_to_seconds(decode_value({0,{1500000}}, "schedule_at", {})) must return exactly sa.tag. That assertion fails on the old code and passes now — it is the proof the silent path is closed.

5. stdb.url(host, database) → (base_url, database) (+4/-4)

The parameter must be a scheme-qualified origin, and the README believed the name: its Returns column claimed ws:// was prepended. It is not — stdb.url("127.0.0.1:3000", "chat") returns a scheme-less string and the connect fails. The name had already caused a documentation bug. Zero call sites, since it is positional.

6. protocol.decode_typed(encoded, spec, …) → (encoded, def, …) (+16/-15)

A schema entry had three names on the frozen surface: def, defs, spec. The singular/plural pair wins 2:1; spec was the outlier, published in the README twelve lines from t.def. protocol.lua:1115 marks it FROZEN (v1.0), so this was the last chance. Zero call sites. Two user-facing error messages now say "schema def".

7. protocol.duration_to_seconds → time_duration_to_seconds (+11/-11)

The type is TimeDuration, the tag string is "time_duration", the wrapper is __time_duration_micros__, and five sibling helpers are named after their types exactly. This was the one abbreviation.

8. subscription_error → subscription_failed (+4/-4)

Three _failed events, one _error. Among failure events it was one _error against three _failed, sitting beside sub.error and sub.state == "error" — the same collision item 1 exists to reduce. (procedure_result is also an exact wire mirror and survives, so "no other event mirrors a wire name" would have been false; the failure-family argument is the one that holds.) evt.type == "subscription_failed" now diverges from sub.state == "error" for the same occurrence, deliberately: it matches reducer_failed vs call.state == "error". Wire tag untouched.

BREAKING for 0.1 consumers

schedule_at_to_seconds now returns "Interval"/"Time" where 0.1 returned "interval"/"time". Compare against the tag (kind == v.tag) rather than a literal, or update the literal.

This repo has no CHANGELOG — release.yml publishes the tag annotation as the release body — so this line and the tag annotation are the only notice that cohort gets. Item 4 is a bug fix and a silent breaking change, and saying only the first half would be the same failure mode this PR exists to remove, pointed the other way.

The direction is still right. Lowercasing the decoder instead would break the larger cohort: cached-row .tag readers, the common path since #35, plus reducer parameters and procedure returns. Independently corroborated — dev/server/index.js declares t.enum("Status", ["Idle", "Busy"]) and the published schema says idle/busy, so 2.8.2 lowercases module enums but not the SDK-builtin ScheduleAt. The inconsistency is the server's; decode_value's one rule — tag is the declared variant name — is right, and the helper was the outlier.

Rejected

evt.counts → evt.changes. Defensible as-is, and it appears in the README's opening example and in every tutorial written against 0.1. Not touched anywhere.

Also not done: renaming sub.conn / t.conn to _conn. The _ privacy convention genuinely is inconsistent there (sub carries _applied/_error beside a bare conn), but that is a ninth rename nobody accepted. Documented instead.

Three freeze decisions

stdb.connection's re-export — all 12 functions are API. Document, do not narrow. stdb/init.lua drives exactly six of them (new, set_send, handle_frame, resubscribe_all, settle_pending, backoff_delay), which makes init.lua the reference transport and those six precisely the seam anyone replacing it needs — LuaSocket, another engine, a test harness. settle_pending and backoff_delay looked like plumbing, but init.lua calls both directly, so a replacement transport cannot work without them. New README section documents the six with signatures verified against the source; the other six are the db: request methods in module form.

db.connection_id was populated, public and undocumented. Now in the Connection-methods table, including the part the field name does not say: it is a fresh ConnectionId per socket, so it changes across a reconnect while db.identity does not.

sub.queries, sub.conn, t.conn were all populated, public and undocumented — frozen whether documented or not. All three documented: sub.queries is genuine API (it is what a reconnect replays, and subscription_replaced already publishes the same strings), and the two .conn are named as back-references a consumer does not need because it already holds db.

Counts, corrected

The freeze review's estimates were close but not exact, and where they disagreed the actual count won. db.state was 55 field lines, not ~48 — the review missed README:288-289 and two test_tables lines and misplaced one README reference. undescribed[].rows was higher too, because the local accumulator and an obsolete comment came with it. Items 7 and 8 matched exactly.

Verification

6 passed, 0 failed on the committed tree; stylua clean. Native conformance PASS both runs through PHASE5 with identity persisted across a process restart. HTML5 PASS both loads through PHASE5 with identity surviving a reload. No fixture drift — chat_schema.lua matches what gen-schema.sh derives from the freshly published module.

Straggler grep is clean: zero hits for subscription_error, [^_]duration_to_seconds, \bspec\b, variant("interval", variant("time", M.url(host, type = "error". There are four wire-rows sites in stdb/ (connection.lua:725, :733, protocol.lua:58, :60), all correctly untouched. They belong to SubscribeApplied/UnsubscribeApplied, not OneOffQueryResponse — an earlier draft of this body misattributed them.

The API surface freeze review from #24's release gate, actioned. **Eight renames, because the tag makes every name permanent and this is the last cheap moment.** No behaviour changes except one silent-failure bug that the naming inconsistency was hiding. A freeze review enumerated every public name — every module, function, parameter, event `type` string, table field and handle method — and counted every call site. Eight were worth the churn; one was rejected. ## The eight **1. Event `type = "error"` → `"socket_error"`** *(+2/-1)* It was the **only emitted event type absent from the README's event table** — 16 rows for 17 types. An undocumented string literal in a frozen surface is the worst possible artifact: undiscoverable and unchangeable. It was also the most generic name on the surface given to the most specific case (one transport's error callback), sitting beside `evt.error`, `sub.state == "error"` and `call.state == "error"` — four unrelated meanings of one token. Every sibling socket event is a participle. The table is now **17/17**, enumerated by hand, including the three dynamically-built sites (`connection.lua:764`, `:899`, `:934`) that a literal grep cannot see. **2. `db.state` → `db.cache`** *(+122/-114)* The connection's cache was `db.state`, while the two handles a caller holds at the same moment spell their lifecycle `sub.state` / `call.state`: ```lua if sub.state == "applied" then stdb.cache.get(db.state, ...) end ``` This is the exact defect this release already fixed once — `connection.lua:264` says *"Named `state` and not `outcome` to match `Subscription.state`: one concept deserves one name."* The same rule condemned `db.state`. `tables.lua:22` already confessed it in prose. `cache.lua`'s own first parameter was renamed too; leaving it would have kept the word in play. Verified **zero cache-ish `.state` field accesses survive** — every remaining one is sub/call lifecycle or a fixture row column literally named `state`. Test-local `state`/`*state` variables in `test_cache.lua` and `test_fixtures.lua` are left alone: they are not surface, and ~30 lines of test churn at a freeze buys nothing. **3. `undescribed[].rows` → `dropped`** *(+22/-27)* A count named as a plural noun. `README:811-816` spent six lines apologising for it, and the apology was the proof: *"The `.row` on an insert or delete event is the row itself; these are different fields."* `e.row` (a row) and `evt.rows` (a number) differed by one character and by kind. Moved in all three places at once — the event, `counts.undescribed[i]`, `sub.undescribed[i]`. **Both apologies deleted**, including a five-line paragraph in `connection.lua` that the rename makes obsolete. **4. `schedule_at_to_seconds` returns `"Interval"`/`"Time"`** *(+22/-19)* **This one was a bug, not a naming preference.** `decode_value` yielded `{tag="Interval"}` while the helper returned `"interval"`, and module-declared sums are server-lowercased (`{tag="busy"}`). So a consumer learned "tags are lowercase", wrote `tag == "interval"`, and got `false` forever with nothing raised. Fixed the **helper**, not the tag: `Interval`/`Time` are the server's own declared variant names (`gen-schema.sh:210` matches them off the live schema endpoint), so the decoder was right and the lowercasing was this SDK's invention. `protocol.lua:275-277`'s apology comment is deleted. The reproduction is now a permanent guard: `schedule_at_to_seconds(decode_value({0,{1500000}}, "schedule_at", {}))` must return exactly `sa.tag`. **That assertion fails on the old code and passes now** — it is the proof the silent path is closed. **5. `stdb.url(host, database)` → `(base_url, database)`** *(+4/-4)* The parameter must be a scheme-qualified origin, and **the README believed the name**: its Returns column claimed `ws://` was prepended. It is not — `stdb.url("127.0.0.1:3000", "chat")` returns a scheme-less string and the connect fails. The name had already caused a documentation bug. Zero call sites, since it is positional. **6. `protocol.decode_typed(encoded, spec, …)` → `(encoded, def, …)`** *(+16/-15)* A schema entry had three names on the frozen surface: `def`, `defs`, `spec`. The singular/plural pair wins 2:1; `spec` was the outlier, published in the README twelve lines from `t.def`. `protocol.lua:1115` marks it FROZEN (v1.0), so this was the last chance. Zero call sites. Two user-facing error messages now say "schema def". **7. `protocol.duration_to_seconds` → `time_duration_to_seconds`** *(+11/-11)* The type is `TimeDuration`, the tag string is `"time_duration"`, the wrapper is `__time_duration_micros__`, and five sibling helpers are named after their types exactly. This was the one abbreviation. **8. `subscription_error` → `subscription_failed`** *(+4/-4)* Three `_failed` events, one `_error`. Among **failure** events it was one `_error` against three `_failed`, sitting beside `sub.error` and `sub.state == "error"` — the same collision item 1 exists to reduce. (`procedure_result` is also an exact wire mirror and survives, so "no other event mirrors a wire name" would have been false; the failure-family argument is the one that holds.) `evt.type == "subscription_failed"` now diverges from `sub.state == "error"` for the same occurrence, deliberately: it matches `reducer_failed` vs `call.state == "error"`. Wire tag untouched. ## BREAKING for 0.1 consumers `schedule_at_to_seconds` now returns `"Interval"`/`"Time"` where 0.1 returned `"interval"`/`"time"`. Compare against the tag (`kind == v.tag`) rather than a literal, or update the literal. This repo has no CHANGELOG — `release.yml` publishes the tag annotation as the release body — so this line and the tag annotation are the only notice that cohort gets. Item 4 is a bug fix **and** a silent breaking change, and saying only the first half would be the same failure mode this PR exists to remove, pointed the other way. The direction is still right. Lowercasing the decoder instead would break the larger cohort: cached-row `.tag` readers, the common path since #35, plus reducer parameters and procedure returns. Independently corroborated — `dev/server/index.js` declares `t.enum("Status", ["Idle", "Busy"])` and the published schema says `idle`/`busy`, so 2.8.2 lowercases module enums but **not** the SDK-builtin `ScheduleAt`. The inconsistency is the server's; `decode_value`'s one rule — tag is the declared variant name — is right, and the helper was the outlier. ## Rejected **`evt.counts` → `evt.changes`.** Defensible as-is, and it appears in the README's opening example and in every tutorial written against 0.1. Not touched anywhere. Also **not** done: renaming `sub.conn` / `t.conn` to `_conn`. The `_` privacy convention genuinely is inconsistent there (`sub` carries `_applied`/`_error` beside a bare `conn`), but that is a ninth rename nobody accepted. Documented instead. ## Three freeze decisions **`stdb.connection`'s re-export — all 12 functions are API. Document, do not narrow.** `stdb/init.lua` drives exactly six of them (`new`, `set_send`, `handle_frame`, `resubscribe_all`, `settle_pending`, `backoff_delay`), which makes `init.lua` the reference transport and those six precisely the seam anyone replacing it needs — LuaSocket, another engine, a test harness. `settle_pending` and `backoff_delay` looked like plumbing, but `init.lua` calls both directly, so a replacement transport cannot work without them. New README section documents the six with signatures verified against the source; the other six are the `db:` request methods in module form. **`db.connection_id`** was populated, public and undocumented. Now in the Connection-methods table, including the part the field name does not say: it is a fresh ConnectionId **per socket**, so it changes across a reconnect while `db.identity` does not. **`sub.queries`, `sub.conn`, `t.conn`** were all populated, public and undocumented — frozen whether documented or not. All three documented: `sub.queries` is genuine API (it is what a reconnect replays, and `subscription_replaced` already publishes the same strings), and the two `.conn` are named as back-references a consumer does not need because it already holds `db`. ## Counts, corrected The freeze review's estimates were close but not exact, and where they disagreed the actual count won. `db.state` was **55** field lines, not ~48 — the review missed `README:288-289` and two `test_tables` lines and misplaced one README reference. `undescribed[].rows` was higher too, because the local accumulator and an obsolete comment came with it. Items 7 and 8 matched exactly. ## Verification `6 passed, 0 failed` on the committed tree; stylua clean. Native conformance PASS both runs through PHASE5 with identity persisted across a process restart. HTML5 PASS both loads through PHASE5 with identity surviving a reload. No fixture drift — `chat_schema.lua` matches what `gen-schema.sh` derives from the freshly published module. Straggler grep is clean: zero hits for `subscription_error`, `[^_]duration_to_seconds`, `\bspec\b`, `variant("interval"`, `variant("time"`, `M.url(host`, `type = "error"`. There are four wire-`rows` sites in `stdb/` (`connection.lua:725`, `:733`, `protocol.lua:58`, `:60`), all correctly untouched. They belong to `SubscribeApplied`/`UnsubscribeApplied`, not `OneOffQueryResponse` — an earlier draft of this body misattributed them.
api: eight renames the 1.0 freeze makes permanent
All checks were successful
ci / ci (pull_request) Successful in 19s
7f7c128040
The tag freezes every public name for good, so each of these is now-or-never.
None is cosmetic; each fixes a name that misleads, hides, or silently fails.

1. evt.type "error" -> "socket_error". It was the ONLY emitted event type
   missing from the README table - 16 rows for 17 types - and an undocumented
   string literal is the worst freeze artifact there is: undiscoverable and
   unchangeable. It also spent the most generic name on the surface on the most
   specific case (one transport's error callback), while `evt.error`,
   `sub.state == "error"` and `call.state == "error"` all mean something else.
   Its siblings from the same callback are participles - connected,
   disconnected, reconnecting, reconnected - and now so is this. The missing
   README row is added, so the table is 17/17.

2. db.state -> db.cache. The connection's cache was `state` while the two
   handles a caller holds at the same moment spell their lifecycle `sub.state`
   and `call.state`: one token, two concepts, in one function body. This
   release already fixed this exact defect once - connection.lua's "Named
   `state` and not `outcome` to match `Subscription.state`: one concept
   deserves one name" - and tables.lua already confessed the other half in
   prose. `db.cache` also reads right at the callsite:
   `stdb.cache.get(db.cache, "player", db.identity)`. cache.lua's own first
   parameter moves with it, because leaving that half-done keeps the word in
   play; the README's stdb.cache signatures now spell it `db.cache` too, which
   is what a consumer actually types.

3. undescribed[].rows -> dropped. A count named as a plural noun, one character
   from `e.row`, which is a row. The README spent five lines apologising for it
   ("it reads as a plural because it is one of `counts`") and connection.lua
   another five; `dropped` says what the caller wants to know - how many did I
   lose - and cannot be mistaken for a payload. Both apologies delete. Moved in
   all three places at once: the event, counts.undescribed[i], sub.undescribed[i].

4. schedule_at_to_seconds returns "Interval"/"Time", not lowercase. This was a
   silent-failure bug, not a naming preference. `decode_value` yields
   { tag = "Interval" } while the helper returned "interval", and a
   module-declared sum really does decode to the server-lowercased variant
   ({ tag = "busy" }) - so a consumer learns "tags are lowercase", writes
   `when.tag == "interval"`, and gets false forever with nothing raised. The
   decoder is right: Interval/Time are the server's declared variant names
   (gen-schema.sh matches them off the live schema endpoint), so the helper's
   lowercasing was the SDK's own invention. Fixed there, apology comment gone,
   and a guard asserts kind == decode_value(...).tag so it cannot come back.

5. stdb.url(host, database) -> (base_url, database). The parameter must be a
   scheme-qualified origin; the name said host, and the README believed the
   name - its Returns column claimed `ws://` was prepended. It is not, and
   `stdb.url("127.0.0.1:3000", "chat")` yields a URL that cannot connect. The
   one entry here whose name had already produced a documentation bug. Zero
   call sites: positional.

6. protocol.decode_typed(encoded, spec, ...) -> (encoded, def, ...). A schema
   entry had three names on the frozen surface: `def` (t.def), `defs`
   (schema.new(defs)) and `spec`. The singular/plural pair wins 2:1 and `t.def`
   is published twelve README lines from `spec`. protocol.lua marks this
   function FROZEN (v1.0), so there is no later. Zero call sites: positional.

7. protocol.duration_to_seconds -> time_duration_to_seconds. The type is
   TimeDuration, the tag string is "time_duration", the wrapper is
   __time_duration_micros__, and five sibling helpers are named after their
   types exactly. This was the one abbreviation, and the only one of the six a
   reader could take for something generic.

8. subscription_error -> subscription_failed. Three `_failed` events, one
   `_error`, and nothing about a rejected query is more error-than-failure than
   a rejected reducer. It does mirror the wire's SubscriptionError, but no other
   event type mirrors a wire name (`transaction`, not TransactionUpdate), so
   that is not a convention this surface keeps.

Deliberately NOT renamed: evt.counts -> evt.changes. Defensible as "the counts
object, which also carries the events", and it is in the README's opening
example and every tutorial written against 0.1.

Freeze scope, decided rather than left implicit:

  * stdb.connection is re-exported and the README called it only "the pure
    core", which froze 12 undocumented functions. All 12 are genuinely API:
    stdb/init.lua drives exactly six of them (new, set_send, handle_frame,
    resubscribe_all, settle_pending, backoff_delay), which makes it the
    reference transport and those six the seam anyone replacing it needs. They
    get a `stdb.connection` reference section; the other six are the `db:`
    request methods in module form, which the section now says out loud.
  * db.connection_id was populated and public but absent from the connection
    table. Documented, including that it changes per socket while the identity
    does not.
  * sub.queries, sub.conn and t.conn were populated, public and undocumented.
    Documented - the two back-references named as such rather than renamed,
    since a `_conn` rename is not on this list.

Suite 6/6, stylua clean, native and HTML5 conformance both pass, no fixture
drift.
api: fix a renamed name that survived in shipped prose, and state the freeze boundary
All checks were successful
ci / ci (pull_request) Successful in 1m30s
5385125334
Review of #55 found one real survivor of the db.state -> db.cache rename,
and it ships: stdb/init.lua's facade example still told readers that a table
named `state` could collide with the connection's own field. After the
rename no such field exists, so half the sentence described an impossible
collision and pointed at something gone. Every sibling copy had been updated
(tables.lua, connection.lua, README) — a grep for `db.state` could not reach
this one because it is the bare word in prose. It is in the first 30 lines of
the first file a consumer opens.

The same review observed the freeze's own criterion — populated, public and
undocumented is frozen whether documented or not — was applied to four fields
and then stopped. Nine more are reachable on the connection, `reconnect_pending`
being the sharpest since stdb.connect invents it and nothing mentions it. Rather
than freeze nine internals by documenting them as API, the README now names them
as explicitly outside the freeze, so the table above it is the whole contract.

Also unified the namespace illustration on one name: it demonstrated the
reachable clash with `close` and resolved it with `cache`, while the test that
guards it uses `cache`. All seven names were verified served by a live 2.8.2.
yuris merged commit 4ee2a24b26 into main 2026-08-20 08:38:42 +00:00
Author
Owner

Review — automated reviewer, posted for the record

Verdict: READY TO MERGE AND TAG after two one-line fixes (both since applied in 5385125). No blockers. All eight renames are correct — none is a worse name, none is churn. Every claim I could execute, I executed.

One real survivor, and it ships

stdb/init.lua:24 still read "a table named `state` or `close` cannot collide with the connection's own". After the rename the connection owns no field named state, so half the sentence names an impossible collision and points at something gone. Every sibling copy was updated (tables.lua:12, connection.lua:41, README:311) — a grep for db.state could not reach this one because it is the bare word in prose.

git archive --worktree-attributes is README + game.project + patches/ + stdb/, so this is in the first 30 lines every consumer downloads.

Undisclosed breaking change

Item 4 is a bug fix and a silent breaking change. A consumer using only the helper and comparing against the README-documented "interval" worked on 0.1 and now gets false forever with nothing raised — the exact failure mode this PR exists to remove, pointed the other way. There is no CHANGELOG (release.yml publishes the tag annotation as the release body), so the PR body is the changelog.

The direction is still right, and better-evidenced than the PR stated: chat_schema.lua:86 tags all_types.sched as schedule_at, and gen-schema.sh reaches that tag by exactly one route — sorted variant names ["Interval","Time"] read verbatim off the live schema endpoint. If 2.8.2 published lowercase there the tag would be unknown and the drift diff would fail; it passed. Meanwhile dev/server/index.js:20 declares t.enum('Status',['Idle','Busy']) and the published schema says idle/busy. So the server lowercases module enums but not the SDK-builtin ScheduleAt — the inconsistency is the server's, decode_value's rule is right, and the helper was the outlier. Lowercasing the decoder instead would break the larger cohort.

Verified by execution, not taken on trust

  • The item-4 guard genuinely fails on origin/main. Same expression both trees: old → sa.tag=Interval, kind=interval, equal FALSE; new → TRUE.
  • The collision guard still guards. Simulated the regression it exists to catch (made tables.install also write facades flat onto the connection): FAIL test_tables.lua "db.cache is the cache even with a table named cache": expected table, got nil. Identical strength to the pre-rename form; not defanged.
  • The 2.8.2 fact is preserved, and cache verified for the first time. I published a scratch module declaring seven tables named after connection fields — cache, close, state, tables, send, identity, pending — and 2.8.2 accepted all seven and served a row from each. No fixture drift.
  • The event table genuinely is 17/17, enumerated by hand including the three dynamically-built sites a literal grep cannot see (connection.lua:764, :899, :934).
  • db.connection_id genuinely changes per socket against a live server while db.identity does not, and the six documented seam functions behave as documented.

Should-fix in the audit trail

The Verification paragraph had three inaccuracies, all corrected in the body: the surviving wire-rows sites belong to SubscribeApplied/UnsubscribeApplied, not OneOffQueryResponse; there are four, not two; and "zero cache-ish .state survives" holds only for the dotted field form — test-local state/*state variables remain and are correctly left alone.

Item 8's stated argument was also false: procedure_result is an exact snake_case mirror of its wire tag and survives this PR, so there were two mirrors. The rename is still right on the argument that holds — among failure events it was 1 _error against 3 _failed.

The freeze criterion was applied and then stopped

"Populated, public and undocumented is frozen whether documented or not" was applied to four fields. I enumerated every object a consumer holds: Subscription, Call and the table accessor are complete. The connection is not — nine more remain, reconnect_pending sharpest of all since stdb.connect invents it at init.lua:236/241, it is on no opts table, and nothing mentions it anywhere. Fixed by naming them as outside the freeze rather than freezing nine internals as API.

On the rejections

evt.counts → evt.changes: agree with rejecting. changes is no more accurate (undescribed is not a change), and the real complaint — two numbers and two lists under one noun — is only fixed by flattening, which is a behaviour change, not a rename.

sub.conn/t.conn → _conn: agree, but for a better reason than the PR gives. "A ninth rename nobody accepted" is process. The substantive reason is that _conn would narrow the frozen surface after the fact — declaring a currently-public field private — which is exactly what README:312-314 condemns.

Sanity

6 passed, 0 failed; stylua clean. Native conformance PASS both runs through PHASE5, run 2 tokenless with identity persisted across a process restart; no fixture drift. HTML5 PASS both loads through PHASE5, identity survived a reload. (First HTML5 attempt failed on net::ERR_UNSAFE_PORT — my port choice, not the diff.)

## Review — automated reviewer, posted for the record **Verdict: READY TO MERGE AND TAG** after two one-line fixes (both since applied in `5385125`). No blockers. **All eight renames are correct — none is a worse name, none is churn.** Every claim I could execute, I executed. ### One real survivor, and it ships `stdb/init.lua:24` still read *"a table named \`state\` or \`close\` cannot collide with the connection's own"*. After the rename the connection owns no field named `state`, so half the sentence names an impossible collision and points at something gone. Every sibling copy **was** updated (`tables.lua:12`, `connection.lua:41`, `README:311`) — a grep for `db.state` could not reach this one because it is the bare word in prose. `git archive --worktree-attributes` is README + game.project + patches/ + stdb/, so this is in the first 30 lines every consumer downloads. ### Undisclosed breaking change Item 4 is a bug fix **and** a silent breaking change. A consumer using only the helper and comparing against the README-documented `"interval"` worked on 0.1 and now gets `false` forever with nothing raised — the exact failure mode this PR exists to remove, pointed the other way. There is no CHANGELOG (`release.yml` publishes the tag annotation as the release body), so the PR body **is** the changelog. The direction is still right, and better-evidenced than the PR stated: `chat_schema.lua:86` tags `all_types.sched` as `schedule_at`, and `gen-schema.sh` reaches that tag by exactly one route — sorted variant names `["Interval","Time"]` read verbatim off the live schema endpoint. If 2.8.2 published lowercase there the tag would be `unknown` and the drift diff would fail; it passed. Meanwhile `dev/server/index.js:20` declares `t.enum('Status',['Idle','Busy'])` and the published schema says `idle`/`busy`. **So the server lowercases module enums but not the SDK-builtin ScheduleAt** — the inconsistency is the server's, `decode_value`'s rule is right, and the helper was the outlier. Lowercasing the decoder instead would break the larger cohort. ### Verified by execution, not taken on trust - **The item-4 guard genuinely fails on `origin/main`.** Same expression both trees: old → `sa.tag=Interval`, `kind=interval`, equal **FALSE**; new → **TRUE**. - **The collision guard still guards.** Simulated the regression it exists to catch (made `tables.install` also write facades flat onto the connection): `FAIL test_tables.lua "db.cache is the cache even with a table named cache": expected table, got nil`. Identical strength to the pre-rename form; not defanged. - **The 2.8.2 fact is preserved, and `cache` verified for the first time.** I published a scratch module declaring seven tables named after connection fields — `cache`, `close`, `state`, `tables`, `send`, `identity`, `pending` — and 2.8.2 accepted all seven and served a row from each. No fixture drift. - **The event table genuinely is 17/17**, enumerated by hand including the three dynamically-built sites a literal grep cannot see (`connection.lua:764`, `:899`, `:934`). - **`db.connection_id` genuinely changes per socket** against a live server while `db.identity` does not, and the six documented seam functions behave as documented. ### Should-fix in the audit trail The Verification paragraph had three inaccuracies, all corrected in the body: the surviving wire-`rows` sites belong to `SubscribeApplied`/`UnsubscribeApplied`, not `OneOffQueryResponse`; there are **four**, not two; and "zero cache-ish `.state` survives" holds only for the dotted field form — test-local `state`/`*state` variables remain and are correctly left alone. Item 8's stated argument was also false: `procedure_result` is an exact snake_case mirror of its wire tag and survives this PR, so there were two mirrors. The rename is still right on the argument that holds — among **failure** events it was 1 `_error` against 3 `_failed`. ### The freeze criterion was applied and then stopped "Populated, public and undocumented is frozen whether documented or not" was applied to four fields. I enumerated every object a consumer holds: `Subscription`, `Call` and the table accessor are **complete**. The connection is not — nine more remain, `reconnect_pending` sharpest of all since `stdb.connect` invents it at `init.lua:236/241`, it is on no opts table, and nothing mentions it anywhere. Fixed by naming them as outside the freeze rather than freezing nine internals as API. ### On the rejections **`evt.counts` → `evt.changes`: agree with rejecting.** `changes` is no more accurate (`undescribed` is not a change), and the real complaint — two numbers and two lists under one noun — is only fixed by flattening, which is a behaviour change, not a rename. **`sub.conn`/`t.conn` → `_conn`: agree, but for a better reason than the PR gives.** "A ninth rename nobody accepted" is process. The substantive reason is that `_conn` would **narrow** the frozen surface after the fact — declaring a currently-public field private — which is exactly what `README:312-314` condemns. ### Sanity `6 passed, 0 failed`; stylua clean. Native conformance PASS both runs through PHASE5, run 2 tokenless with identity persisted across a process restart; no fixture drift. HTML5 PASS both loads through PHASE5, identity survived a reload. (First HTML5 attempt failed on `net::ERR_UNSAFE_PORT` — my port choice, not the diff.)
Sign in to join this conversation.
No description provided.