SkirRPC official-runtime parity audit

Copy Markdown View Source

Audit date: 2026-10-03. This document records the protocol behavior used to shape the client extracted from skir-elixir-gen 0.3.0.

Reference implementations

The audit compared the current official SkirRPC implementations rather than only the prose documentation:

  • TypeScript: skir-client 1.0.19, src/skir-client.ts
  • Dart: skir_client 1.0.12, lib/src/skir_client.dart
  • Gleam: skir_client 1.0.5, service/client modules under src/skir_client

TypeScript and Dart agree on the strict request/error surface in most places. Where those two agree and Gleam differs, this Elixir implementation follows the TypeScript/Dart behavior. This is a compatibility choice, not a claim that the Gleam implementation is invalid.

One narrow TypeScript/Dart difference is retained deliberately: TypeScript accepts any JavaScript number in the manual JSON envelope's method field, whereas Dart requires an integer method ID. Elixir follows Dart here because Skir method IDs are integers and the compact wire syntax is integer-only.

Behavior matrix

BehaviorTypeScriptDartGleamElixir client 0.2.0
list, studio, and empty-body keywordsExact; no trimExact; no trimTrims bodyExact
Compact method-number syntaxStrict signed decimalStrict signed decimalMore permissiveStrict signed decimal
Non-empty method numberDispatch by numberDispatch by numberDispatch by number when parsedDispatch by number
Empty method-number slotLookup by nameLookup by nameLookup by nameLookup by name
Unknown methodHTTP 400, detailedHTTP 400, detailedHTTP 404, genericHTTP 400, detailed
Duplicate method numbersRegistration errorRegistration errorRegistration errorRegistration error
Duplicate method namesAllowed; name-only request becomes ambiguousSameName map overwritesAllowed; ambiguous name-only request is HTTP 400
Method lookup vs typed request decodeLookup firstLookup firstDifferent implementation pathLookup first
JSON-envelope numeric methodAny JS numberIntegerInteger-oriented decoderInteger (follows Dart)
text/plain error media typeIncludes charset=utf-8Includes charset=utf-8Plain text/plainIncludes charset
Controlled error with no custom messageStandard HTTP reason phraseStandard HTTP reason phraseRuntime-specific APIStandard HTTP reason phrase
Unknown handler errorsHidden by defaultHidden by defaultHidden/default policyHidden by default
Unknown-error disclosure controlBoolean or predicateBoolean or callback policyRuntime-specific APIBoolean or predicate
Response serialization failureVisible HTTP 500 messageVisible HTTP 500 messageDifferent error pathVisible HTTP 500 message
Studio title/body shellRPC StudioRPC StudioSkir StudioTypeScript/Dart form
list method numberCurrent 1.0.19 source anomaly: emits method nameNumeric IDNumeric IDNumeric ID
Reflection type envelopetype + recordsCompatibleCompatibleCompatible
Client POSTYesYesYesYes
Client GETYes, optional; WHATWG URL.search encodingNo public equivalent in audited clientNo public equivalent in audited clientYes; matches TypeScript query encoding
Non-2xx client messageHTTP status N[: text]SameExposes text differentlyTypeScript/Dart form
Successful response preserves unknown valuesYesYesYesYes
Default server error loggerConsolestderr-style loggingNo-opNo-op (documented policy divergence)

TypeScript list anomaly

In skir-client 1.0.19 the TypeScript list implementation currently constructs an entry equivalent to:

method: methodImpl.method.name
number: methodImpl.method.name

The number field therefore contains a string method name. Dart and Gleam emit the numeric method ID, which is also what Studio/reflection consumers expect. Elixir deliberately emits the numeric ID. The executable parity oracle normalizes only this known TypeScript anomaly so it will continue working if upstream fixes the line.

Executable parity oracle

npm run test:rpc-parity instantiates the official TypeScript Service from skir-client and a native Elixir Skir.RPC.Service with equivalent primitive methods. It feeds both services the same raw requests and compares status code, content type, and response body. Cases include:

  • numeric and name dispatch;
  • JSON-envelope dispatch and leading whitespace;
  • strict invalid method numbers;
  • unknown-method precedence over malformed typed request JSON;
  • malformed request JSON for a known method;
  • standard controlled-error messages;
  • exact list / studio keyword behavior;
  • duplicate-name ambiguity and numeric disambiguation.

The list response is compared semantically after normalizing the one documented TypeScript number anomaly. The malformed request JSON case checks the HTTP status, content type, and RPC error prefix, allowing V8 and the built-in JSON parser to use their own syntax-error diagnostics. All other cases compare the transport-neutral raw response directly.

The oracle is intentionally a hard gate: missing Node dependencies, mix, or Elixir fails the command rather than silently skipping cross-runtime comparison.

Runtime verification

On 2026-10-04 the oracle passed all 16 raw server cases and GET/POST client wire checks on Elixir 1.20.4 / OTP 29. CI runs the complete gate on pinned OTP 27, 28, and 29 releases. To reproduce with the normal toolchains:

npm ci --ignore-scripts
mix local.hex --force
mix local.rebar --force
npm run test:all

A live Dart client ↔ Phoenix endpoint test remains a separate deployment gate, especially for middleware, authentication, proxy behavior, and application timeouts.