feat(rest): spec-generated REST surface (client + typed I/O + wire tests) for the TypeScript SDK - #140
Merged
Merged
Conversation
mjerris
marked this pull request as draft
June 27, 2026 20:15
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
enumerate-signatures used checker.typeToString(), which EXPANDS a type alias to its definition — so a generated alias like `CallResponse = CallLeg | FabricDeviceLeg` (from *.types.generated) was recorded as `union<...CallLeg,...FabricDeviceLeg>`. The Python reference's griffe enumerator keeps the alias NAME (`CallResponse`), so the two ports' oracles disagreed on the same identical type (38 spurious drifts). When a return/param type's `aliasSymbol` is declared in a *.types.generated file, emit the alias by its fully-qualified generated-module name (`class:signalwire.<mod>.<AliasName>`) instead of expanding it. This matches how Python records the same generated alias, and the porting-sdk diff checker normalizes both ports' generated-type refs by leaf name. Regenerated port_signatures.json. Source unchanged — recording convention only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WIP checkpoint (#81): enumerate-signatures emits crud_base for CrudResource<...> subclasses (records binding by written type name, not resolved); datasphere binds DocumentUpdateRequest (was Partial<Create>). TS drift=0 against dual-form oracle. Local checkpoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…racle) Adopt the named-subclass shape for fabric resources so the port matches the Python reference oracle, via a TS resource generator parallel to the Python one. - generate-rest-types.ts: add groupResources + emitResourcesModule, emitting fabric.resources.generated.ts — one named CRUD subclass per full-CRUD resource (extends FabricResource/PUT<...> with a typed create(body, extras?) / update(id, body, extras?)). GEN-FRESH-gated like the type modules. - base/FabricResource.ts: move FabricResource / FabricResourcePUT here (out of the fabric namespace) so the generated subclasses can extend them without a cycle; drop the redundant constructors (the inherited one suffices). - fabric.ts: construct the generated subclasses; remove AutoMaterializedWebhookResource and its deprecation warning (pre-release SDK, nothing to deprecate — direct webhook create is a normal operation). - index.ts: export the generated resource classes. - tests: webhook create no longer warns. Result: tsc clean, eslint clean, GEN-FRESH passes, 2908 tests pass, and the port matches the Python oracle (drift = 0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…T surface roll) Roll Python's full REST-surface generation to TS. The generator now reads the x-sdk-resource markup (name/base/kind/collection/update_method/methods/set_methods/exclude) instead of inferring from paths, with all three emitters: operation-methods, command-dispatch (calling), set_methods (relay_rest). Added the ReadResource base; extended to all 13 namespaces. video pilot landed (hand classes deleted, video.ts uses the generated classes; tests updated + green). Remaining (this roll): client-tree generation (RULES §8), the ×11 namespace adoption, drift cleanup (int/float handled via --numeric-monotype in run-ci.sh). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…REST drift 0 Port RULES §8 client-tree generation: generate-rest-types.ts now resolves placement markup (x-sdk-namespace.attr, per-resource namespace/attr, flat default) across all specs and emits _client_tree_generated.ts (6 container classes + a _GeneratedResourceTree wiring base, camelCase TS idiom). RestClient extends it + _wireResources(http), keeping only auth/HTTP construction + hand compat. Mirrors Python's _client_tree_generated.py + client.py. All 11 remaining namespaces adopted: each hand .ts is now a thin shim re-exporting the generated resource classes (+ back-compat aliases) and the container from the tree; hand resource/container class bodies deleted. No behavioral hand code lost (verified vs the Python oracle). Enumerator: skip _-prefixed (private) classes; map ReadResource/FabricResource -> rest._base; fold .resources.generated -> _resources_generated in the surface enumerator; union-member-order fix for command-dispatch params (dial.codecs). Verified: tsc 0 errors; 2656 non-compat tests pass (252 compat-mock fail are pre-existing, no compatibility spec); REST non-compat signature drift = 0; GEN-FRESH deterministic. 4 documented wire-identical port divergences (ReadResource ctor, RestClient.compat, live_transcribe/translate nested-union spelling). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
Delete the compat surface completely, mirroring Python's removal: - src/rest/namespaces/compat.ts + compatibility.types.generated.ts. - RestClient (rest/index.ts): drop the CompatNamespace import, the `compat` member + construction, and the Compat* re-export block. - src/index.ts: drop compat docstring examples. - tests: delete all compat_*_mock.test.ts + compat.test.ts; remove the compat assertion + the compat-scoping test in client.test.ts; drop `['compat','compat']` from the namespace list and fix the count (21→20); remove the compat assertions in IndexTopLevel.test.ts. - examples: delete rest/examples/rest-compat-laml.ts; remove the compat block in rest/examples/rest-client.ts + the compat dispatch cases in examples/rest_audit_harness.ts. - generator: drop the `compatibility` spec-map entry. This removes the ~252 pre-existing compat-mock test failures (no compatibility spec in this porting-sdk) AND clears the compat signature drift — REST drift is now fully 0 (incl compat). Verified: tsc 0 errors (main + examples); 2607 tests pass (0 fail); REST drift 0; GEN-FRESH clean; no compat code remains. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…wire tests (recovered) Two parallel agents' work, recovered after a crash (processes lost, on-disk work intact + green): SWAIG typed surface (D): generate SwaigRequest + the PostPrompt payload tree from the authoritative vendored swaig-specs/ (was sourced from the old swml-webhooks). New SwaigContracts.generated.ts + SwaigContracts.ts. Re-point every consumer (SWMLService, AgentBase, SwaigFunction, SkillBase, prefabs, skills, types) from PlatformContracts' SwaigRequestData/PostPromptData to the new SwaigRequest/PostPrompt. REST test generation (E): new scripts/generate-rest-tests.ts emits 12 *_generated.test.ts (full-mock wire tests per route, success + error, independent oracle = route-registry x spec operationId). Verified after recovery: tsc 0 errors; 3025 tests pass (127 files; +418 generated wire tests over the prior 2607). Drift/GEN-FRESH verification to follow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
… SWAIG drifts) The TS enumerator only walked class + function declarations, so the already-generated SwaigContracts.generated.ts interfaces (SwaigRequest, PostPrompt, and their sub-entry types) were invisible to the drift oracle — the Python reference enumerates each TypedDict's CLASS-typed fields as zero-arg members, so 11 PostPrompt/SwaigRequest fields showed as missing-port. collectInterface() now walks the generated-payload modules' exported interfaces and projects the same surface Python does: reuse signatureFromProperty (class-typed fields only, primitives skipped) to emit each class-typed field as a zero-arg member. Restricted by file path (SwaigContracts.generated / swml_verbs_generated) so no other interface in the codebase leaks into the oracle. With the diff tool's gen-payload module fold, the 11 SWAIG read-side payload fields now match Python field-by-field: 378 -> 367 drift. The remaining 367 are the swml_verbs typed surface (AIParams/AIObject/… — Python types every SWML $defs schema, TS's older generateVerbTypes.ts types only 2 verbs richly); that's the next step. Build clean (0 tsc). 3025 tests unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…yload drifts) Port the SWML-verbs typed-surface generator from the Python reference (generate_python_rest_types.generate_swml_verbs) to the TS main generator, closing the gen-payload DRIFT gaps. generate-rest-types.ts: - add generateSwmlVerbs(): one TS interface per schema.json $defs object schema (type alias for non-object schemas) + the flattened <Verb>Config interfaces walked from $defs.SWMLMethod.anyOf, emitted into src/swml_verbs_generated.ts (filename chosen so its module folds to the diff's gen-payload marker). Open- shaped read payloads (every field optional, [key:string]:unknown tail), same lint bar as hand source. Wired into main() (prebuild + --check GEN-FRESH). - refName(): resolve #/$defs/<Name> refs by leaf (was components/schemas only). - tsType(): honor x-sdk-enum-literal / x-sdk-widen markup and external .json refs (-> Record<string,unknown>), mirroring py_type. Inert for the REST path. enumerate-signatures.ts (record generated-payload interface fields like the Python griffe enumerator -- by name, not type-checker-inlined): - treat aliases declared in gen-payload files (swml_verbs_generated.ts) as named class refs so SWMLVar/AIPostPrompt survive instead of collapsing to string. - collectInterface(): resolve each field from its WRITTEN type node (so `boolean | SWMLVar` keeps its class arm), key fields verbatim (not snake-cased: allOf, numberedBullets), skip ALL-CAPS names (SWAIG) -- all matching enumerate_python_signatures.py. - generatedAliasFromNode(): handle literal / inline-object / multi-member-null union members so class-bearing written unions stay recordable. - translateType(): an anonymous inline object (__type) -> dict<string,any> (matching py_type), not a bogus class:__type. - factor the SDK-class-ref test into isSdkClassRef() so the written-node path and signatureFromProperty apply the identical filter (no over-recording of list<list<class>>). PORT_SIGNATURE_OMISSIONS.md: document SkillMixin.list_skills (a pre-existing hand-method shape divergence surfaced once inline-object fields stopped mis-recording as class:__type). Residual 7 gen-payload drifts are genuine Python-vs-TS shape differences (5 are Python's left-associative `|` AST nesting that the diff tool doesn't flatten; 2 are the else Python-keyword fields Python can't name but TS legitimately emits) -- left for human sign-off rather than silenced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…erved-word fields) The generated SWML-verb configs type `else?: SWMLMethod[]`, a field the Python reference's TypedDict generator must drop (`else` is a Python keyword → recorded as a `# non-identifier field` comment). TS object keys have no such restriction, so the port is more faithful to the wire than the reference can express — the read-side analog of from->from_. Documented in PORT_ADDITIONS.md (the legitimate port-has-more mechanism), not silenced as an omission. Resolves the last 2 SWML-verbs gen-payload drifts. Remaining 5 drifts are pre-existing RELAY/logging baseline (get_execution_mode + relay.call.* Pausable/Stoppable/Volume), untouched by this work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
… a [mode, logMode] tuple Python's get_execution_mode() returns just the execution-mode string; the log-mode is derived separately in configure_logging (cgi->off else default). TS had folded both into one function returning a 2-tuple [string, 'off'|'stderr'|'default'] — a return-shape divergence vs the reference (the last non-RELAY drift). getExecutionMode() now returns the bare string. The richer TS log-mode mapping moves to an internal getDerivedLogMode() helper (kept — TS routes lambda->stderr etc., more than Python's cgi->off). Call sites updated: deriveSuppressed / deriveStreamFromMode use the helper; isServerlessMode + the Logger tests read the string directly. Drift now 0 (signatures match). Build clean, 3025 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…t across examples/docs/surface The compat-removal (a5030d5) and SWAIG type re-point left artifacts the DRIFT-only gates didn't see; a full run-ci.sh surfaced them. Fixes: - examples: re-point the renamed SWAIG types (SwaigRequestData->SwaigRequest from SwaigContracts, PostPromptData->PostPrompt for the onSummary rawData param) in session-state + skills_audit_harness; drop the now-dead WithHttp/HttpClient in rest_audit_harness. (LINT: tsc+eslint clean.) - docs: delete rest/docs/compat.md; remove the compat example block from guide.md and the client.compat row from client-reference.md. (DOC-AUDIT clean.) - port_surface.json regenerated — drops the stale compat namespace surface (SURFACE-FRESH / SURFACE-DIFF: the committed surface predated compat removal). - REST_COVERAGE_GAPS.md: drop the stale compatibility.list_available_phone_number_resources_by_country allowlist entry (the compat spec no longer exists — non-canonical). - FMT: prettier reformat of generate-rest-types.ts + SwmlVerbMethods.generated.ts (the auto-fix the FMT gate applies; committed so CI --check stays green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…ions The surface gate (diff_port_surface.py) has no structural skip, so TS's concrete-action control methods (PlayAction.stop/pause/resume/volume, etc.) read as additions vs the reference surface, which emits them on the abstract mixin bases (StoppableAction/PausableAction/VolumeAction) the concrete actions inherit. TS flattens the hierarchy and inlines the methods onto each concrete action — where the caller invokes them. Same idiom the signature gate excuses via _is_abstract_action_base_method; documented here (17 entries) as the surface gate's mechanism is markdown excusal. Matches the existing _base.__init__ inheritance-idiom precedent in this file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
Make the SURFACE-DIFF gate actually compare the generated REST/SWML/SWAIG type surface (it was comparing nothing — enumerate-surface.ts walked only class/function/const, never interface/type, so 0 generated types were surfaced). 753 -> 23 SURFACE-DIFF deltas via: - enumerate-surface.ts: collectTypeDefinition emits each exported interface/type in the generated-type files as a bare symbol, scoped by isGeneratedTypeFile (no blanket interface walk). - name-keyed method-leak guard: a method-less generated-type interface skips the inherited/Python method augmentation, so the POM Section/DataMap BUILDER methods stop spraying onto the same-named SWML-schema interfaces. - pickModule precedence: reference class->module candidates win over the file alias (fixes SecurityConfig, which lives in SWMLService.ts but the reference places in security_config). - TS_MODULE_ALIASES: src/swml_verbs_generated.ts -> signalwire.core.swml_verbs_generated (fold the module path to the reference's). - generate-rest-types.ts: operationAliases skips emitting <OpId>Request/Response when the schema is a bare $ref to an already-named type (~240 redundant aliases removed — surface noise the reference doesn't carry; the alias is kept only for inline op bodies). Build clean (0 tsc), 3025 tests pass. Remaining 23 deltas (RELAY abstract action bases, SWAIG action types, swml_webhooks payload placement) are small follow-ups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…CallHandler - enumerate-surface.ts: scope PlatformContracts.generated.ts into the generated-type walk and alias it to signalwire.rest.namespaces.swml_webhooks_types_generated (the reference's module for those SWML/SWAIG webhook payload types) — surfaces the 9 PostPrompt/Swaig payload types. - PORT_OMISSIONS.md: the RELAY abstract action mixin bases (Stoppable/Pausable/VolumeAction + their methods) that TS flattens onto concrete actions, and PhoneCallHandler (hand-written enum in TS, generated in the reference). SURFACE-DIFF now 2 (was 753): the residual PlaybackBgAction/TransferAction are a real gap — TS has no generate_swaig_actions emitter for the SWAIG response-action config types. Build clean, 3025 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…g_actions analog) TS had no emitter for the SWAIG response-action CONFIG types (TransferAction, PlaybackBgAction, HoldAction, ContextSwitchAction) — Python generates them from swaig-response.yaml. Add generateSwaigActions (mirrors Python's generate_swaig_actions: lift each object-shaped action value into a <Verb>Action interface) → new src/SwaigActions.generated.ts (4 types), aliased to signalwire.core.swaig_actions_generated and folded by the diff tool. Closes the last genuine SURFACE-DIFF gap (PlaybackBgAction/TransferAction). Build clean, 3025 tests pass. Surfacing the full generated-type surface also newly exposes ~119 port types the reference doesn't carry (idiomatic extracted enums like CallDirection, and generator name-preference types) — previously invisible because the gate compared no types at all. Those are the Category-5 classification pass (idiomatic-keep vs follow-Python-naming), tracked separately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
Final surface-alignment pass, driving SURFACE-DIFF fully clean: - enumerate-surface.ts: skip bare-scalar format aliases (type uuid/docid/jwt = string) to match the reference's surfacing policy (griffe drops module-level scalar TypeAliases); map the built-in-collision renames Record_/Set_ back to the reference's Record/Set. - PlatformContracts.generated.ts scoped + aliased to swml_webhooks_types_generated (9 payload types). - SwaigActions.generated.ts scoped + aliased to swaig_actions_generated. - PORT_ADDITIONS.md: ~110 idiomatic named types the reference inlines (extracted enums, discriminated/permission unions, operation-named request/response types) documented as additions per the keep-the-better-shape rule; CrudResource.get/list (TS folds list/get onto the base). - PORT_OMISSIONS.md: RELAY abstract action mixin bases + PhoneCallHandler (hand-written enum). SURFACE-DIFF: ✓ clean (2181 symbols, 383 excused omissions, 455 excused additions). Build clean, 3025 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
The FMT gate's auto-fix line-wrapped the generated-type guard, the operationAliases/isObj arrows, and the SwmlVerbMethods.generated object literals. Commit the formatted tree so CI's prettier --check stays green. No behavioral change (build clean, gates unaffected). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…rated tests)
The sentinel for an object/interface param emitted a bare {} which fails to type-check against a
closed request type with required fields (TS2345 under strict). Now fill each REQUIRED property
with its own sentinel (recursively — { prompt: { text: 'x' }, agent_id: 'x', name: 'x' }); optional
props are omitted, all-optional objects still yield {}. Regenerated the 12 *_generated.test.ts:
56 tsc errors → 0, 1137 tests still pass. Prereq for pulling tests into the strict type-check (F).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…8 fixes) Toward pulling tests/ into the strict tsc scope (F). AST codemod (scripts/fix-test-strict.ts, diagnostic-driven — inserts ! at the exact TS2532/TS18048 expression span, e.g. tools[0]!.name) resolves the possibly-undefined array/index-access errors in the hand test suite: 898 fixes across 63 files. Behavior-neutral (! is compile-time only); 3025 tests still pass. Test-strict errors 1233 → 335; the residual (unknown mock-body casts, arg shapes) need per-site annotation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
JournalEntry.body stays `unknown` (arbitrary wire payload); lastBody() narrows it to WireBody — a recursively-indexable JSON object — so wire-assertion tests can do (await mock.lastBody()).params.id without an `any` per access site. Infra for the strict-tests residual (the ~93 last.body.X sites in calling_mock + siblings adopt lastBody()). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
calling_mock.test.ts asserts on both last.method/last.path and last.body. Keep `const last = await mock.last()` and add `const wireBody = last.body as WireBody` (imported from mocktest), then assert on wireBody.X. Nested params/detect objects are narrowed with `as WireBody` at the access site. No behavior change; drives 93 TS18046 unknown-access errors to 0.
fabric_mock, video_mock, registry_mock assert on both last.method/path and last.body.X. Narrow the remaining unknown last.body.X accesses with (last.body as WireBody).X (imported from mocktest). Removes the last TS18046 unknown-access errors. registry_mock create_campaign: `usecase` is not a typed spec field (the spec field is sms_use_case), so route the arbitrary wire fields through createCampaign's `extras` escape hatch — matching the sibling campaigns.update test's documented pattern — with an empty typed body. No behavior change to the wire request.
The createMockFetch helper's RecordedRequest.body was `unknown`, forcing 27 TS2571 unknown-access errors in calling.test.ts. SDK REST methods always send a JSON object, so type body as the recursively-indexable WireBody (reused from mocktest) — wire assertions like req.body.command and req.body.params.to type-check without an `any`. Nested params objects are narrowed with `as WireBody` at the two codecs access sites. No behavior change.
…rage
The fabric and video route-coverage suites called several SDK methods with
the wrong argument convention — a real bug strict mode surfaced:
- Positional-arg methods were called with an object literal. Fixed to the
correct positional args:
* fabric.tokens.createEmbedToken/createGuestToken/createInviteToken/
createSubscriberToken/refreshSubscriberToken (object -> string/string[])
* fabric.resources.assignDomainApplication (object -> id string)
* fabric.resources.assignPhoneRoute (object -> phone_route_id + handler;
also fixes missing 3rd required arg)
* fabric.subscribers.createSipEndpoint (object -> username + password;
fixes missing required password arg)
* fabric.subscribers.updateSipEndpoint / cxmlApplications.update
(object -> positional string)
* video.rooms.createStream / conferences.createStream / streams.update
(object -> url string)
* video.roomTokens.create (object -> room_name + user_name positional)
- Object-body *Request methods: video update fixtures used the wrong field
name (`name` -> `display_name`, the real UpdateRoom/UpdateConference field);
create success fixtures made valid (display_name). Remaining route-only
bodies (never asserted; mock synthesizes response from spec and does not
validate the body) are cast to their typed *Request param, documented by a
file-level note in fabric_coverage_mock.
Behavior-neutral: every touched test asserts only method/path/matched_route
(and status for errors), which are unchanged.
Same class of real convention bugs strict surfaced in the relay-rest route- coverage suite. Positional-arg methods were called with object literals or too few args; fixed to correct positional args: - addresses.create (object -> 9 positional required strings) - importedNumbers.create (object -> number + number_type) - mfa.sms / mfa.call (object -> to string), mfa.verify (object -> token) - numberGroups.addMembership (object -> phone_number_id string) - verifiedCallers.submitVerification (object -> verification_code string) - shortCodes.update (object -> name + message_handler; fixes missing 3rd arg) - sipProfile.update (object -> domain_identifier string) - registry.campaigns.update (object -> name string) - registry.campaigns.createOrder (object -> phone_numbers string[]) Object-body *Request create calls (route-only bodies, never asserted) are cast to their typed param (PurchasePhoneNumberRequest, CreateVerifiedCallerID Request, CreateNumberGroupRequest, CreateManagedBrandRequest, CreateManaged CampaignRequest), documented by a file-level note. Behavior-neutral: assertions check only method/path/matched_route/status.
…ocks
small_namespaces_mock:
- addresses.create used address_type 'commercial', which is not a member of
the AddressType enum. Replaced with a valid member ('Suite') in both the
call and the assertion (real invalid-enum bug).
- `const sent = last.body || {}` narrowed sent to `{}`, breaking every
sent.X access. Retyped as `(last.body ?? {}) as WireBody`.
small_specs_coverage_mock: positional-arg methods were called with a single
object literal. Fixed to positional args:
- project.tokens.create (object -> name + permissions), update (-> name)
- calling.dial (object -> from + to)
- chat.createToken / pubsub.createToken (object -> ttl + channels)
Behavior-neutral (assertions unchanged aside from the corrected enum value).
Reconcile the TS port against the new porting-sdk oracle (3434643) which requires the framework-free decomposed webhook-validation core `webhook_middleware.validate(method, url, headers, body, *, signing_key) -> optional<(status, headers, body)>`. - Add `validate()` to src/WebhookMiddleware.ts: framework-free decision core that returns null (pass) or a [status, headers, body] rejection triple. Honors the X-Twilio-Signature alias, case-insensitive header lookup, throws on a missing signing key, never throws on a missing/bad signature. - Refactor the Hono `webhookValidationMiddleware` wrapper to delegate to `validate()` so the adapter and the cross-port core share one implementation. The Hono wrapper stays a PORT_ADDITION idiom. - enumerate-signatures.ts: record `validate` under FREE_FN_PARAM_OVERRIDES so `signing_key` reads as keyword-only (mirrors Python's `*, signing_key`), matching the oracle param kind — the same teach-the-checker projection as RestClient. Types are the port's real ones. - Delete the now-obsolete `validate_request` optional<union> vs union<...,void> typed-divergence omission: the oracle now emits `optional<union<...>>`, which is type-compatible with the port's union<...> via the comparator — reconciled through the type-map, not an omission. - Add 6 tests proving the decomposed core: valid -> null, bad -> 403 triple, missing header -> 403 (no throw), X-Twilio-Signature alias, case-insensitive lookup, missing key throws. The FastAPI `make_webhook_validation_dependency` impossible: entry stays (it is the framework wrapper idiom, no cross-port shape). DRIFT + SURFACE-DIFF pass; webhook impossible-count for the decomposed core is 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…le omits it)
porting-sdk 3434643 added webhook_middleware.validate to the SIGNATURE oracle
but deliberately left the SURFACE oracle unchanged ("surface unchanged (module
function)") — python_surface.json still lists only make_webhook_validation_
dependency. The prior commit's surface regen leaked validate into port_surface
.json, tripping SURFACE-DIFF (port symbol not in reference).
Add SURFACE_FUNCTION_EXCLUSIONS to enumerate-surface.ts to drop validate from
the surface (mirroring griffe's surfacing policy the same way bare-scalar type
aliases are dropped), while port_signatures.json keeps it for DRIFT. Now both
SURFACE-DIFF and DRIFT reconcile against the oracle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…ace oracle omits it)" This reverts commit fdc5924.
…oracle 5a5c8aa)
porting-sdk 5a5c8aa committed the surface-oracle regen the Pass-1 signatures
commit (3434643) had left uncommitted, adding two surface symbols every port
must now match:
- signalwire.core.security.webhook_middleware.validate (the decomposed core)
- signalwire.core.web.HostAppRouter (latent from the as_router pass — the
surface side was never committed until now)
enumerate-surface.ts:
- drop the short-lived SURFACE_FUNCTION_EXCLUSIONS (reverted): the surface
oracle DOES record validate, so the port surface must include it.
- map src/web.ts -> signalwire.core.web and surface the HostAppRouter marker
type alias as a zero-method class (MARKER_TYPE_ALIASES allowlist), matching
the reference's named-marker surfacing.
SURFACE-DIFF + DRIFT both reconcile against the oracle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…racle fba1b19) porting-sdk fba1b19 EXCLUDES core.web.HostAppRouter from the surface oracle: it is a signature-only named type reconciled via type_aliases, NOT a surface class. Revert the enumerate-surface.ts HostAppRouter surface projection added in 5cc7d39 (the MARKER_TYPE_ALIASES set, the non-generated type-alias branch, and the src/web.ts -> signalwire.core.web module alias) so core.web is absent from port_surface.json. Kept: src/web.ts HostAppRouter=Hono type alias (drives the SIGNATURE side, still present in port_signatures.json) and webhook_middleware.validate in the surface (the decomposed core, unchanged). sw-verify typescript: DRIFT + SURFACE-DIFF PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Pass-2 RELAY action-contract reconcile against porting-sdk @ 5744580, which projects the StoppableAction/PausableAction/VolumeAction mixin methods directly onto the concrete actions instead of exposing the base types cross-port. - Add pause(behavior?)/resume() to CollectAction (real gap: play_and_collect is a VolumeAction in the reference and requires pause/resume, which TS lacked). - Give PlayAction.pause an optional `behavior` param, matching the reference PausableAction.pause(behavior: str | None) shape (RecordAction already had it). - Drop the obsolete base-class prose block (PORT_OMISSIONS) and the 17 floating per-action control-method additions (PORT_ADDITIONS): the reference now requires those methods on the concrete actions, so they MATCH, not add. - Regenerate port_surface.json / port_signatures.json. - Extend tests/relay/Action.test.ts: CollectAction stop/pause/resume/volume, pause behavior forwarding on Play + Collect, and record-has-no-volume. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
The port already ships the TLS-serving primitives (SecurityConfig with
sslCertPath/sslKeyPath + validateSslConfig, plus the SslConfig helper class).
Only the FORM differed: the reference exposes the config as a primitive
path dict via SecurityConfig.get_ssl_context_kwargs() ({ssl_certfile,
ssl_keyfile}), which the "returns an ssl.SSLContext" framing mischaracterized —
python already returns only primitive path strings.
- Add SecurityConfig.getSslContextKwargs() -> Record<string, string>,
mirroring the oracle verbatim: {ssl_certfile, ssl_keyfile} path strings when
SSL is enabled and validates, else {}. Snake-cases to the reference's
get_ssl_context_kwargs and matches its owning class (SecurityConfig)
directly, so no enumerator rename entry is needed.
- Delete the impossible: omission for
SecurityConfig.get_ssl_context_kwargs (the capability is present, only the
return shape had been recorded as a substitute SslConfig.getServerOptions
addition — which remains a legit node:https helper).
- Regenerate port_surface.json + port_signatures.json.
- Add a test asserting the returned dict exposes primitive cert/key PATHS
(not PEM contents, not an object).
DRIFT + SURFACE-DIFF pass. get_ssl_context_kwargs impossible count now 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…(pass 3+6+7) (A) Pass 3 — handle_request: expose the framework-free request-dispatch core as SWMLService.handleRequest(method, url, headers, body) -> [status, headers, body] (mirrors Python's SWMLService.handle_request and dotnet's HandleRequest), plus the AgentBase override that renders via renderSwml. Both are enumerated onto the oracle's swml_service/agent_base modules (surface + signatures); the AgentBase signature is recorded as an addition since the signatures oracle dedups the inherited override to the base (same pattern as AgentBase.render_swml). Align the routing-callback signature to (body, headers): RoutingCallback gains an optional headers arg and both callsites (SWMLService + AgentBase Hono handlers) now pass the request headers, matching Python's decomposed callback_fn(body, headers). (B) Pass 6 — type_inference: reclassify the create_typed_handler_wrapper and infer_schema entries from `impossible:` (which falsely claimed no oracle equivalent) to honest ts-idiom rationales — the oracle now emits both. TS returns an InferredSchema struct where Python returns a positional 5-tuple, and create_typed_handler_wrapper takes an explicit param_names list because JS erases parameter names; both are the static-typed rendering of the same helper. Added a test building a SWAIG param schema from a typed tool handler. (C) Pass 7 — BUG cleanup: verified every focus area (security_utils, logging_config, webhook_validator, PaginatedIterator, data_map factories, prefab on_summary, list_skills, SkillBase.setup/register_tools/define_tool). All are correctly present-and-emitted or kept as genuine divergences — zero false omissions to delete. sw-verify typescript: DRIFT + SURFACE-DIFF + GEN-FRESH* all PASS. Signature omissions now 0 impossible / 0 banned (287 idiom). Full run-ci green on the tracked tree (LINT/TEST/REST-COVERAGE pass; the only failures are in the pre-existing untracked compat_accounts_mock.test.ts from separate WIP). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
#61: the served path (serve->getApp->honoServe, asRouter->getApp) re-implemented auth/render inline and returned routing as a WRONG 200 {action:redirect} instead of the decomposed handleRequest core's real 307. Added serveViaHandleRequest(c) adapter; handleSwml + handleRouting now delegate to handleRequest and return the real 307/401/200. Mirrors rust/dotnet/php served-path shape. No signature change. Served-path 307 test (tests/AgentBase.test.ts): hits the actual served /sip, asserts 307+Location — FAILED before (got 200), PASSES after. + 401 bad-auth, 200 happy-path. sw-verify PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…xpose Adds the Tier-2 behavioral-contract native tests (porting-sdk BEHAVIORAL_CONTRACTS.md, contracts 2-6) and fixes the TypeScript stubs each test exposes, so the TEST gate now forces the real behavior. Fixes: - Serverless dispatch (contract 5): decode isBase64Encoded proxy bodies before routing; add CGI dispatch (ServerlessAdapter.buildCgiEvent + createCgiHandler reconstruct the request from the CGI env/stdin and route through Hono). runServerless builds the CGI event from the environment when dispatched in cgi mode. Was Lambda-only with base64 bodies mangled and CGI never reconstructed. - SIP routing dispatch (contract 6): enableSipRouting now REGISTERS a routing callback at /sip that extracts the SIP username and consults _sipUsernames (was stored-but-unconsulted). AgentServer.setupSipRouting wires its unified server callback onto each agent with the correct (body, headers) signature and re-mounts so the /sip route is reachable; register() now registers routing callbacks before mounting (mount snapshots routes, so a post-mount callback was invisible). - SecurityUtils.validateUrl: honor SWML_ALLOW_PRIVATE_URLS (parity with Python's validate_url), needed for a loopback remote search server. - AgentBase.serveViaHandleRequest: cast the served-path status to ContentfulStatusCode (pre-existing #61 tsc error on this branch). Contracts 2 (set_prompt_llm_params merge) and 3 (InfoGatherer submit_answer state machine) and 4 (native_vector_search remote HTTP POST) were already real; tests added to lock the behavior. Each new test fails against the stub it replaces (verified). port_surface.json / port_signatures.json / PORT_ADDITIONS.md regenerated: ServerlessAdapter gains build_cgi_event + create_cgi_handler (TS port-only helpers, no Python equivalent). sw-verify typescript + full run-ci PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…fillers); fix constant-time token compare
Contract 7 (tool-token wire format): lock-in tests for the 5-field
`{call_id}.{function_name}.{expiry}.{nonce}.{signature}` token — decoded
5 fields + non-empty nonce, two mints => different nonces, a python-oracle
16-hex-nonce token validates in-port (nonce read positionally, no length
assumption), tampered signature rejected. Fixed a real defect: validateToken
compared the signature with `!==` (short-circuits at the first differing
character, leaking bytes via timing). Now routes through crypto.timingSafeEqual
with a length guard, matching Python's hmac.compare_digest.
Contract 8 (AI/LLM structured fillers): lock-in tests that addPatternHint
survives as a structured {pattern, replace, ignore_case} object (not a bare
string) and addLanguage carries engine + speech_model + fillers into the
rendered SWML ai.languages entry.
Each test fails against the pre-fix / degraded body; full run-ci PASS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
… surfaced
Adds the TypeScript port's Layer-D dump programs (scripts/{wire,swml,state,http,
wire-relay}-dump.ts) so the porting-sdk differs can byte-compare the port against
the python oracle. Each emits {case_id: artifact} canonical JSON on stdout,
dispatching each corpus case to the native TS API. RELAY frames are captured
in-process by intercepting the client's execute() (the interpreted-port analog of
the python oracle's _RecordingCall/_RecordingClient), no mock WebSocket needed.
All 5 differs PASS. Real port divergences surfaced + fixed:
- SessionManager token nonce was 32 hex chars (randomBytes(16)); python uses
secrets.token_hex(8) = 16. Now randomBytes(8).
- AgentBase.addLanguage dropped the `model` field; python emits it under
engine/model. Added `model` to LanguageConfig + emission.
- renderSwml always emitted ai.prompt as {text}, even in POM mode; python emits
{pom:[...]} when in POM mode with sections and no raw prompt string. Now mirrors
agent_base's prompt_is_pom branch.
- AgentBase.extractSipUsername was missing the tel: branch (returned the whole
"tel:+1..." field); now strips the scheme like SWMLService.extractSipUsername.
- Serverless/Hono 401 challenge body was Hono's plain "Unauthorized" text; python
returns the JSON envelope {"error":"Unauthorized"}. Added invalidUserMessage.
Full run-ci.sh gate green (TEST/LINT/FMT/DRIFT/SURFACE + all Layer-D differs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Add 5 sched_gate lines (BEHAVIORAL-WIRE/SWML/STATE/HTTP/WIRE-RELAY) that diff each surface dump against the python oracle, modeled on the EMISSION gate. Each --dump-cmd bakes in SIGNALWIRE_LOG_MODE=off so the dump emits ONLY JSON on stdout regardless of the caller's ambient env. Resolve signalwire-python via a PYTHON_SDK_DIR ($PYTHON_SDK env override, else repo-adjacency) mirroring diff_port_emission.py's _resolve_python_sdk, and pass it explicitly with --python-sdk so the oracle build never depends on ambient sys.path. Full run-ci: all 5 new gates PASS, no regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…d now optional) porting-sdk's rest-apis/fabric/openapi.yaml was fixed so create requests no longer require the server-assigned identity field. Regenerated the REST types and wire tests off the fixed spec: - AIAgentCreateRequest.agent_id: required -> optional - SipEndpointCreateRequest.id: required -> optional - generated fabric wire tests drop the agent_id:'x' / id:'x' placeholders they previously had to pass to satisfy the (now-gone) required constraint GEN-FRESH, GEN-FRESH-TESTS, DRIFT, REST-COVERAGE, tsc, and vitest all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…lient boundary
swml-dump: add swml_define_tool_complete_schema — define_tool with a complete
{type,properties,required} schema renders ai.SWAIG.functions[?function=lookup]
.parameters as that schema FLAT (pass-through). No double-wrap in TS:
normalizeParameters already returns a full schema unchanged.
wire-relay-dump: observe verb frames at the CLIENT-SEND boundary. Each case now
uses a fresh frames buffer + recording client and emits _no_frame_transmitted
when the verb never reaches client.execute, mirroring the oracle _run_verb (a
build-but-never-transmit fails instead of returning a stale per-method frame).
TS Call already transmits via _client.execute, so no transmission bug surfaced.
Both Layer D differs PASS; full run-ci PASS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Fix the 6 day-one porting-audit gates so each exits 0 without --report-only: - doc_lang_purity: rewrite the 4 python code fences in relay/RELAY_IMPLEMENTATION_GUIDE.md into real TypeScript (dial(), handleEvent(), executeOnCall(), Message shape). - doc_links: repoint CHECKLIST.md's phone-binding.md link to rest/docs/phone-binding.md; drop rest/README.md's dead link to the excised examples/rest-compat-laml.ts. - root_hygiene: replace the three hardcoded machine paths in scripts/_env.sh, scripts/run-ci.sh (nvm node bin -> $SW_NODE_BIN, CI uses setup-node) and scripts/enumerate-signatures.ts (/usr/local/home fallback -> ../porting-sdk adjacency); add ROOT_HYGIENE_ALLOW.md excusing the 14 load-bearing audit-contract/artifact files read at repo root by porting-sdk scripts. - ignore_ledger_verify: prune 136 stale DOC_AUDIT_IGNORE.md entries whose Python doc blocks were already rewritten to TS (name absent from all scanned docs); keep only the 35 still-referenced external/wire identifiers. - meta_consistent: add repository+homepage URLs to package.json. - artifact_deny: package already ships only dist/+README via package.json files: allowlist; npm-pack listing goes clean through --listing mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Wire the Day-one gates into the scheduler as BLOCKING (non-report-only) checks: DOC-LANG-PURITY, DOC-LINKS, ROOT-HYGIENE, IGNORE-LEDGER-VERIFY, META-CONSISTENT, ARTIFACT-DENY. ARTIFACT-DENY uses the authoritative --listing mode: dayone_artifact_deny feeds the real published npm package listing (npm pack --dry-run --json, files[].path) to artifact_deny.py, rather than the git-ls-files proxy that over-reports in-repo files excluded from the package via package.json "files". All 6 gates pass green; full run-ci PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
The HttpClient sent a hardcoded `@signalwire/sdk-ts/2.0.0` User-Agent — both the wrong product token and a stale version (package is at 2.0.5). Derive the UA at runtime from package.json via createRequire, building `signalwire-typescript/<version>` so it can never drift from a literal again. Mirrors the Python reference's `signalwire-python/<v>` (_base.py _user_agent, which reads importlib.metadata at runtime). `../../package.json` resolves identically from src/rest/ (tsx dev) and dist/rest/ (installed), since package.json ships at the package root. Test now asserts the derived value equals `signalwire-typescript/<pkg.version>`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
public_jargon (64 -> 0): - TEMPLATE: generate-swml-verbs.ts no longer emits the "WEAK GROUNDING" jargon in the say_gender doc-comment; reworded to describe the accepted values for a user. Regenerated SwmlVerbMethods.generated.ts. - Hand-written TSDoc (27 files) reworded to be user-facing: "Python parity"/"parity" -> "matches / equivalent to the Python SDK"; "the TS port" -> "this SDK"; internal repo paths (porting-sdk/webhooks.md, swaig-specs, rest-apis) -> plain SignalWire spec descriptions; removed references to the audit harness, the signature oracle, and the FREE_FN_NAME_OVERRIDES table from user-visible docs. gen_idiom (1 exclusion -> clean): - TEMPLATE: generate-swml-verbs.ts emitted a stale eslint-disable no-empty-interface directive that no longer suppressed anything (the generated interfaces are all non-empty), tripping reportUnusedDisableDirectives once linted. Removed it + regenerated. - eslint.config.mjs no longer ignores src/SwmlVerbMethods.generated.ts; the SWML verb-method augmentation is now linted as real source like the rest of the generated TS. Full run-lint.sh + run-ci.sh PASS. gen_type: already clean (no change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Wire GEN-TYPE-DEGENERACY, PUBLIC-JARGON, GEN-IDIOM, and RELEASE-FRESH into scripts/run-ci.sh as blocking (non-report-only) gates, mirroring the Day-one gate idiom. All four exit 0 enforcing; run-ci PASS. ROUTE-COLLISION is intentionally NOT wired: ts has no default route-registry command the gate can consume (route_collision.py self-skips for typescript). Wiring it needs a registry builder for the gate first — follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Add a --parse-only flag (alias --dry-run) to the swaig-test CLI that validates an invocation's arguments and exits WITHOUT loading the agent, touching the filesystem, or making any network request. Valid args print exactly "parse OK" and exit 0; invalid args (unknown flag, missing required positional) exit non-zero. This mirrors the Python reference (commit 44f4692) so the cross-port DOC-CLI gate can validate documented swaig-test invocations exactly. - Detected + stripped from argv FIRST (stripParseOnly), so it is position-independent — honored whether it precedes or trails an --exec (which otherwise consumes trailing tokens as the function name/args). - Short-circuits in main() after parseArgs validation but before any agent load / network access; prints exactly "parse OK". - parseArgs no longer prints usage-and-exit-0 for `--parse-only` alone: that is a missing-required-positional error and now falls through to the agent-path error path (non-zero exit, no "parse OK"). - 4 subprocess tests: valid/alias/after-exec (position-independent) + the invalid-arg rejection path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Anchor the three TypeScript quickstart blocks (agent, RELAY, REST) to real,
gate-compiled fixtures under examples/ via <!-- include: --> markers, so the
README code can no longer rot — it IS the compiled code, asserted byte-identical
at gate time (porting-sdk readme_include.py).
- New fixtures examples/quickstart-{agent,relay,rest}.ts, each a real runnable
example with a `// region: construct` span.
- tsconfig.examples.json: map `@signalwire/sdk` -> ./src/index.ts so the fixtures
can use the public import name (byte-identical to the README) and still
typecheck in-tree under the LINT gate.
- Corrected three wire/API errors in the README REST block to match the real
generated client (verified against the SDK surface):
calling.play(id, { play: [...] }) -> calling.play(id, [...]) (2nd arg is an array)
datasphere.documents.search({ query_string }) -> search('billing policy') (positional)
phoneNumbers.search({ areaCode }) -> phoneNumbers.search({ area_code }) (snake_case wire key)
- DOC_AUDIT_IGNORE.md: add toLocaleTimeString (JS Date built-in) used by the
agent quickstart.
The webhook-verification block is left a plain fence: it is a deliberately
partial illustrative fragment (two alternative `const ok = ...` usages shown
together), not a single compilable program.
readme_include gate: clean (3 include sites verified). run-ci: PASS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
README-INCLUDE now runs as a blocking gate: every doc code block anchored by an <!-- include: --> marker must stay byte-identical to its gate-compiled fixture region. Doc rot for converted quickstart blocks is now impossible — the doc code IS the compiled fixture. Port README already converted + gate clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
SNIPPET-COMPILE (#110) now type-checks each fenced ts block against the built dist/index.d.ts, so `import ... from '@signalwire/sdk'` resolves and deleted/renamed exports or wrong call shapes fail. Report-only went from 675 failures to 0 across 42 docs (529 compiled, 248 illustrative signature/pseudo-code blocks marked no-compile). Two mechanisms: - Page-setup preambles (34): a `<!-- snippet-setup -->` block per page supplying the shared context the prose establishes — `export {};` (top-level await) plus `declare global { const ... }` ambient bindings (`client`, `agent`, `call`, `FunctionResult`, `DataMap`, `process`, ...). The declare-global form is conflict-free: a block with its own `const X`/`import { X }` simply shadows it. - no-compile markers (248): bare API method/type signatures shown as reference (api-reference/swaig-reference/datamap-guide), pseudo-code, and cross-file or uninstalled-package fragments that can't stand alone. Real doc rot fixed against the generated typed REST/relay/skill surface (the docs were written against an older loose/object-arg API): - REST: many methods are positional now, not object-arg — calling.dial(from,to), transfer(callId,dest), play(callId,play[]), *Pause/Resume/Stop(callId,control_id), liveTranscribe/liveTranslate(callId, {start:{...}}); addresses/importedNumbers/ shortCodes/mfa/project.tokens/pubsub+chat.createToken all positional; snake_case body keys and required-field corrections (phone_number_ids->phone_numbers, verifiedCallers {phone_number}->{number}, swmlScripts contents not code, conference display_name required, datasphere.search obj->positional, video createStream/streams positional url); fabric token expire_at is a Unix-second number not an ISO string. - RELAY: call.collect speech.endSilenceTimeout->end_silence_timeout (snake_case), sendMessage {to,from}->{toNumber,fromNumber}, RelayEvent fields under .params. - Skills/agent: SkillBase.setup() returns Promise<boolean> not void; SkillBase logger is this.logger not this.log; SkillSchemaInfo field is parameters not configSchema; dropped non-exported SkillManifest/SkillToolRegistration imports; AgentOptions has no prompt field (use setPromptText); DataMap.parameter 4th arg is {required} not true; addLanguage fillers/functionFillers are keyed Records not bare arrays. - livewire: Agent tools is a FunctionTool[] array, not an object map. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
The widened DOC-AUDIT (#122) flagged two doc references that are external framework APIs, not SDK symbols: - cwd -> process.cwd() (Node.js) in configuration.md ConfigLoader.search - fetch -> app.fetch(request) (Hono) in serverless-guide.md Both are the same class as existing entries (process.exit, Hono app.use). Added honest ledger lines; DOC-AUDIT and IGNORE-LEDGER-VERIFY green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Load porting-sdk/rest-apis/x-sdk-overlay.yaml in the shared generator machinery (_gen-common.ts) and apply it at every top-level named-schema emission site: hidden fields are dropped from the SDK surface entirely (still on the wire), deprecated fields are emitted with a /** @deprecated */ TSDoc marker. `scope` is matched against the SPEC schema name (the $defs / components.schemas key) threaded through declaration / swmlDeclaration / swaigDeclaration — not the emitted TS type name. Regenerated types drop the 5 hidden AIParams fields (audible_debug, audible_latency, verbose_logs, enable_accounting, cache_mode) from calling/fabric REST types and swml_verbs; languages_enabled is kept and marked @deprecated. Regenerated port_signatures.json + port_surface.json. sw-verify typescript: SURFACE-DIFF / DRIFT / GEN-FRESH(*) / NO-LAUNDER all PASS; tsc build clean; 2659 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
…overlay) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adopts the spec-driven generated REST surface for the TypeScript SDK — the REST client, its typed inputs, the read-side payload types, and the full wire-test suite are generated from the canonical specs and matched to the Python reference, with source and tests fully strict-typed.
What the branch delivers
scripts/generate-rest-types.tsreads eachrest-apis/<ns>/openapi.yaml+ itsx-sdk-*markup (REST_GENERATOR_RULES.md) and emits per-namespace*.types.generated.ts+*.resources.generated.ts+ a_client_tree_generated.ts. All 11 namespaces adopted (namespace files reduced to re-export shims); typed inputs are closed params + anextrasdoor; enums default-closed. GEN-FRESH (--check) gates byte-identical regeneration.dcdce60.swaig-specs/; the full SWML verb-config surface generated fromschema.json$defs(swml_verbs_generated.ts); the signature enumerator now walks generated-payload interface fields .*_coverage_mocksuites deleted, with REST-COVERAGE staying green; behavioral*_mock.test.tskept.strict+noUncheckedIndexedAccessclean; the test suite is brought into the strict type-check scope (tsconfig.test.jsonin the lint gate), generated tests strict-clean by construction, hand tests annotated (WireBodyfor typed wire-body assertions).--numeric-monotype(int ≡ float, wire-neutral).CondElse.else) documented as port omissions/additions.Type-alias recording (cross-port oracle agreement)
The signature enumerator records a generated type by its alias name rather than expanding it:
checker.typeToString()expands a type alias to its definition — so a generated alias likeCallResponse = CallLeg | FabricDeviceLegwas recorded asunion<…>. Python's griffe enumerator keeps the alias name, so the two oracles disagreed on the same identical type (38 spurious drifts once Python adopted the generated types). Fix: when a type'saliasSymbolis declared in a*.types.generatedfile, emit it by its generated-module name; the porting-sdk diff checker normalizes both ports' generated-type refs by leaf name. This is a recording convention; the emitted wire is unchanged.Runtime
Unchanged — generated types are static-only (
JSON.parse(text) as T), method bodies still return the raw server JSON; a differently-shaped response is returned unchanged and never throws.Gates
stricttsc (source + tests) · prettier FMT clean · GEN-FRESH pass · REST-COVERAGE green · DRIFT 0 · SURFACE-DIFF 0.Built on
The REST generator + checker-normalization changes on
porting-sdkmain (PR #53), merged. Companion to python #39 (the reference surface the ports follow).🤖 Generated with Claude Code