Skip to content

feat(rest): spec-generated REST surface (client + typed I/O + wire tests) for the TypeScript SDK - #140

Merged
mjerris merged 82 commits into
mainfrom
ci/record-generated-aliases-by-name
Jul 8, 2026
Merged

feat(rest): spec-generated REST surface (client + typed I/O + wire tests) for the TypeScript SDK#140
mjerris merged 82 commits into
mainfrom
ci/record-generated-aliases-by-name

Conversation

@mjerris

@mjerris mjerris commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

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

  • REST surface generated from spec + markup. scripts/generate-rest-types.ts reads each rest-apis/<ns>/openapi.yaml + its x-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 + an extras door; enums default-closed. GEN-FRESH (--check) gates byte-identical regeneration.
  • Twilio-compat (LAML) REST API removed entirely (namespace, generated types, tests, client wiring, examples) to match Python dcdce60.
  • read-side typed payloads. SWAIG request/post-prompt/response-action types from the vendored swaig-specs/; the full SWML verb-config surface generated from schema.json $defs (swml_verbs_generated.ts); the signature enumerator now walks generated-payload interface fields .
  • REST test generation. Generated full-mock wire-test suites (success + error per route); the redundant hand *_coverage_mock suites deleted, with REST-COVERAGE staying green; behavioral *_mock.test.ts kept.
  • strict typing of source AND tests. The whole REST/SWAIG/SWML/agent space is strict + noUncheckedIndexedAccess clean; the test suite is brought into the strict type-check scope (tsconfig.test.json in the lint gate), generated tests strict-clean by construction, hand tests annotated (WireBody for typed wire-body assertions).
  • drift-gate flag. TS is single-numeric-type, so the drift gate runs with --numeric-monotype (int ≡ float, wire-neutral).
  • SURFACE-DIFF alignment of the generated type surface: the surface enumerator emits generated type definitions, redundant operation aliases are suppressed at the emitter, RELAY action mixin bases / reserved-word fields (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 like CallResponse = CallLeg | FabricDeviceLeg was recorded as union<…>. 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's aliasSymbol is declared in a *.types.generated file, 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

strict tsc (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-sdk main (PR #53), merged. Companion to python #39 (the reference surface the ports follow).

🤖 Generated with Claude Code

@mjerris
mjerris marked this pull request as draft June 27, 2026 20:15
mjerris and others added 29 commits July 1, 2026 09:57
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
…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).
mjerris and others added 28 commits July 5, 2026 17:08
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
…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
@mjerris
mjerris merged commit 9847703 into main Jul 8, 2026
3 checks passed
@mjerris
mjerris deleted the ci/record-generated-aliases-by-name branch July 8, 2026 02:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants