Conversation
…m fold)
Deletes the two `PORT_SIGNATURE_OMISSIONS.md` entries whose member is exactly
`logger`. Both excused a symbol that neither side emits any more, so they were
dead paperwork:
- signalwire.core.skill_base.SkillBase.logger
- signalwire.agent_server.AgentServer.logger
Counts (committed tree, measured with the gate's own `parse_omissions`):
PORT_SIGNATURE_OMISSIONS.md 249 -> 247 entries; `logger`-member entries 2 -> 0.
Why each removal is correct (not merely dead): owner ruling 2026-07-24, recorded
in ALLOWLIST_DISCIPLINE.md §8 and implemented at
porting-sdk/scripts/enumerate_python.py:365 (`_LOGGER_FACTORY_RETURN`) —
logging is a MODULE-LEVEL capability a port may reach however its language does.
The per-instance `logger` attribute was Python's structlog idiom leaking into the
enumerated surface; it is not contract. The reference exclusion keys on the
logging factory's RETURN TYPE rather than a hand-typed symbol list, so it cannot
drift as classes are renamed.
Verified dead on BOTH sides, not just the reference:
- oracle: `query_signatures.py python_signatures.json search logger` returns
only `modules.signalwire.core.logging_config.functions.get_logger` — the
module-level factory, no class-attribute `logger`.
- port: the same query over `port_signatures.json` returns the same single
hit; the TS port emits no `.logger` member either.
The capability itself is unchanged and still satisfied by the 5 module-level
free functions the oracle records (get_logger, configure_logging,
get_execution_mode, reset_logging_configuration, strip_control_chars), all of
which the TS port implements. Nothing in `src/` changes.
The excused-divergence count is UNCHANGED at 684 across the deletion, which is
the direct proof the two entries were excusing nothing.
No new omission/addition/allow-list entry was added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
…ff fold) Deletes the 41 `PORT_SIGNATURE_OMISSIONS.md` entries whose member is `__init__` and whose class the reference's `construction` node covers. The shared-diff fold (`_is_folded_dunder_member`, porting-sdk #125) skips those symbols before the ledger is ever consulted, so each entry was excusing nothing. Counts (measured with the gate's own `parse_omissions`): PORT_SIGNATURE_OMISSIONS.md 249 -> 208 entries `__init__` entries 59 -> 18 excused divergences 642 -> 642 (UNCHANGED) The unchanged excused count is the direct proof the 41 were dead paperwork: had any been load-bearing, deleting it would have surfaced a live divergence. The construction contract is untouched. `port_signatures.json` still records 150 construction classes / 536 ctor params before and after — `__init__`-as-a-member and the §10 construction node are different contracts, and only the former is folded. Every one of the 41 classes remains compared by NAME in `compare_construction`, which is the meaningful comparison for a ctor (the member comparison matches params by POSITION, which is meaningless against a TS options-object constructor). 18 `__init__` entries the rule does NOT cover are deliberately RETAINED, because their classes have no `construction` entry — the member comparison is the only one there is, so folding them would trade a visible ledger entry for a real blind spot: signalwire.core.pom_builder.PomBuilder.__init__ signalwire.rest._base.{CrudResource,CrudWithAddresses,ReadResource}.__init__ signalwire.livewire.* (14: Agent, AgentServer, AgentSession, CartesiaTTS, DeepgramSTT, ElevenLabsTTS, LLM, OpenAILLM, RunContext, STT, SileroVAD, StopResponse, TTS, ToolError) Stale rationale found while auditing: the deleted `signalwire.agents.bedrock.BedrockAgent.__init__` entry claimed "reference signatures oracle records no BedrockAgent class". That is no longer true — the oracle's construction node records BedrockAgent with 7 params (max_tokens, name, route, system_prompt, temperature, top_p, voice_id), so the ctor IS compared and the entry was doubly dead. No source change. No new omission/addition/allow-list entry — this is a deletion pass. `PORT_OMISSIONS.md` and `PORT_ADDITIONS.md` are untouched (a different tool with a hard dead-entry gate). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
… signatures The oracle now records 7 DERIVED public `__init__` attributes that are caller-observable VALUES (porting-sdk d7c859d, ALLOWLIST_DISCIPLINE class B2). Six of the seven were ALREADY implemented in this port and only needed the signature artifact regenerated so the enumerator's reference-driven field-accessor rule would project them: SignalWireRestError.request_id -> RestError.requestId (src/rest/RestError.ts) SWMLService.ssl_enabled -> SWMLService.sslEnabled SWMLService.ssl_cert_path -> SWMLService.sslCertPath SWMLService.ssl_key_path -> SWMLService.sslKeyPath SWMLService.domain -> SWMLService.domain Action.completed -> Action.completed (src/relay/Action.ts) The seventh, `SpiderSkill.remove_xpaths`, existed only as a hardcoded literal inside `_fastTextExtract`, so the value was not caller-observable. Promoted it to a public prefilled `readonly removeXpaths: string[]` carrying the reference's exact XPath spelling, and drove the strip loop off the field. Cheerio takes CSS, so `removeTagFor()` maps the `//tag` descendant-axis form to a tag-name selector and passes anything else through. Two tests added: one mirroring python's `test_remove_xpaths_populated` (the list is prefilled, not empty), and one behavioral proof that the field is load-bearing — dropping `//aside` from it leaves aside text in the extracted output. DRIFT: 29 -> 22. All 22 remaining are the pre-existing `RestClient.<resource>` accessor drift from a separate task; zero derived-attr drifts remain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
… served plain HTTP
MY ORIGINAL DIAGNOSIS WAS WRONG and the fix it prescribed would NOT have fixed this.
I inferred from `new SslConfig()` (no arguments) that TS had no config-file layer at
all. It did: SecurityConfig.loadFromConfigFile, wired from the ctor. SslConfig is the
env-defaults object; the config file is applied over it afterward. Adding "the missing
layer" would have left the real defect untouched.
THE REAL DEFECT (reproduced live), three causes:
1. KEY-SHAPE MISMATCH — load-bearing. TS read `security.ssl.{enabled,certPath,keyPath}`;
the reference and every other port use FLAT snake_case `security.{ssl_enabled,
ssl_cert_path,ssl_key_path}` (signalwire-python core/security_config.py:123-127).
An operator writing the documented shape hit a branch that never matched — plain
HTTP, no error, no warning. That camelCase shape appears in no doc, test or example:
dead-on-arrival code, not TS idiom.
2. NO DISCOVERY — the reference falls back to find_config_file(service_name); TS only
honoured an explicitly-passed path.
3. STALE BACKING OBJECT — sslConfig was never rebuilt, so isConfigured() /
validateSslConfig() / getSslContextKwargs() kept reading env paths regardless.
Also aligned to the reference while here: getSection() so ${VAR} interpolation applies,
the non-SSL keys (allowed_hosts, cors_origins, use_hsts, hsts_max_age), and the auth
shape (`security.basicAuth.*` -> reference `security.auth.basic.*`), which was wrong
the same way.
VERIFIED BEHAVIOURALLY, NOT STRUCTURALLY: tests/SecurityConfig.configFileTls.test.ts
starts a real service from a config file with NOTHING in the environment, completes a
real TLS handshake against its socket, and asserts a plaintext client is rejected on
that same port. 7 tests pass.
MUTATION-TESTED (re-run independently by the orchestrator): with the fix stashed,
6 failed / 1 passed — and the HTTPS case fails as the SECURITY BUG itself, not an
assertion mismatch ("SSL routines:tls_validate_record_header:wrong version number"
against a server that logged "starting on http://"). The 7th is the negative control
(plain HTTP when nothing enables SSL) and passes both ways by design.
Adds ZERO public surface (verified by diffing fresh enumerations with and without).
No omission/addition/allow-list entry.
Pre-existing, NOT from this change (identical on clean HEAD): SURFACE (DRIFT,
SURFACE-FRESH, SURFACE-DIFF) and BEHAVIORAL (SECURE-DEFAULT). SURFACE-FRESH is stale
port_surface.json from becf955, which added the SWMLService TLS attrs without
regenerating the artifact.
Refs #90
…four commits behind The committed artifact carried generated_from '@ 34d8804' (the pre-wave consolidation SHA), not the wave tip. A plain regen at the tip emits all 7 derived attrs; the enumerator needed ZERO changes. ORCHESTRATOR CORRECTION: I told this lane a regen produced a byte-identical file and implied the enumerator needed new collection logic. That measurement was mine and it was WRONG — I ran the regen before the wave-tip commit existed, then re-checked after committing it. The lane instrumented the emission pass and proved every target member was already in both the AST-collected set and the reference set. ruby hit the identical stale-artifact cause independently. Non-RestClient SURFACE-DIFF gaps: 7 -> 0. Total 29 -> 22 (the pre-existing RestClient set). SIGNATURE gate unchanged (byte-identical drift list vs HEAD). Tests 2824 pass, FMT/LINT exit 0.
Both TS enumerators walked only a class's OWN members, so every member
declared on a private base was recorded nowhere. `RestClient extends
_GeneratedResourceTree`, and that generated base declares all 22 REST
resource-tree accessors -- `calling`, `fabric`, `video`, every flat resource
and every namespace container. The private base is skipped as an
implementation detail (mirroring griffe's treatment of Python's
`_GeneratedResourceTree`), and nothing lifted its members onto the subclass,
so `port_signatures.json` recorded 1 of 23 members on RestClient and
`port_surface.json` recorded 1 of 23.
That read as 22 missing features on both drift axes. It was not: the
accessors are wired, reachable, and spec-derived. The new
tests/rest/resource_tree_mock.test.ts proves it behaviourally -- each of the
22 makes a real call through the accessor against the shared porting-sdk mock
and asserts the request lands with the expected method + path and matches a
spec route.
The fix is the TS analogue of the reference enumerator's
`_wired_base_attributes` (porting-sdk/scripts/enumerate_python_signatures.py),
keyed on STRUCTURE rather than a name list: for a public class, lift the
public class-typed properties declared on the PRIVATE bases it extends.
Because the base file is generated from porting-sdk/rest-apis/*/openapi.yaml,
the accessor set tracks the specs by construction -- a new resource in the
specs appears in both artifacts with no change to the enumerators.
Scoped narrow, matching the reference's rule:
- private bases only; a public base is its own surface symbol whose members
are already enumerated on the base itself,
- class-typed properties only (a primitive scalar field is internal state),
- private/protected members excluded,
- transitive through a chain of private bases, stopping at the first public
one,
- locally-declared members always win, so an override is never masked.
The private base itself remains absent from the emitted surface, and is kept
out of the name-keyed inheritance maps so it cannot leak through
`resolveInherited`.
Both axes go 22 drifts -> 0 with no ledger entry added: signatures exit 0
(642 excused divergences, unchanged), surface exit 0 (36 excused omissions /
468 excused additions, unchanged). port_surface.json also picks up its
regeneration at the branch tip.
Verification: SIGNATURES exit 0, SURFACE exit 0, run-lint exit 0,
run-format --check exit 0, run-tests exit 0 (135 files, 2847 tests).
The SECURE-DEFAULT gate was RED: scripts/secure-default-dump.ts still spoke the
pre-2026-07-27 protocol, emitting {secure_default_true, wire_reflects_secure} —
two booleans the PORT computed by inspecting its own render. The differ
(porting-sdk/scripts/diff_port_secure_default.py) now rejects that shape outright
rather than tolerating it, because a gate whose comparable is the port's own
conclusion can only re-confirm the port's own premise. That vacuity is how java
shipped a token in `meta_data_token` — the SWML metadata SCOPING key the engine
MD5-derives from public config and never validates — alongside a TOKENLESS
web_hook_url, while its self-classification stayed green.
The dump now emits the wire payload and makes no judgement about it:
{"<fixture id>": {"secure_default_true": bool, "rendered": {<functions[] entry>}}}
* secure_default_true is read back from the LIVE registry
(agent.getTool(name).secure), never the value passed to defineTool — echoing
the input back makes the field incapable of ever failing.
* rendered is the tool's own SWAIG.functions[] entry, verbatim, with token
VALUES replaced by the corpus placeholder <TOKEN>. Keys and key paths are
preserved exactly, so the DIFFER derives has_own_webhook and token_carrier.
Structure mirrors the php port's migrated bin/secure-default-dump (d3cbc47),
including per-fixture agent isolation and the redaction helpers that make the
differ's re-application of redact_entry a no-op.
The TS emitter itself was already correct and is unchanged: AgentBase.ts:2347-2353
is a faithful three-way branch (explicit webhookUrl wins -> else token/query params
build a local URL carrying __token -> else NO web_hook_url key), matching
agent_base.py:1085-1099.
Also tightens Contract 9's wire assertion in the tier-2 behavioral suite. It
previously checked only that the secure URL contains `__token=` and the insecure
URL does not, which is weak in both directions: a token relocated to a
`meta_data_token` field passes it, and an insecure tool handed its own tokenless
per-tool webhook — an unauthenticated function-specific callback — passes it too.
The new case pins the topology the differ derives: the token rides ONLY as a
__token query param on the function's own webhook, no other token-suffixed key is
present, and an insecure tool has no web_hook_url property at all. Verified by
mutating the emitter to the java shape (token -> meta_data_token, webhook always
emitted): the pre-existing assertions all still passed; only the new one caught it.
Verification:
SECURE-DEFAULT gate before: exit 1 (2 FAIL, both "LEGACY self-classifying dump")
SECURE-DEFAULT gate after: exit 0 ("PASS - typescript")
Mutation-tested the dump 4 ways, each red with the correct diagnosis:
token -> meta_data_token field -> "TOKEN IN THE WRONG PLACE: field:meta_data_token"
drop the secure web_hook_url -> "NO TOKEN ON THE WIRE"
give the insecure tool a webhook-> "WEBHOOK SHAPE: expected NO per-tool web_hook_url"
__token -> token query param -> "TOKEN IN THE WRONG PLACE: webhook_query:token"
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
porting-sdk 90164e9 unified the drift checker to compare `type`, `kind`,
`required` and `default` on ALL params, not just `__init__` ones. That surfaced
6 live divergences in typescript. Each is a real port divergence — TypeScript
expresses optional params, default parameter values and union-with-undefined,
so none of these was a language ceiling.
WebService.start(host) — default-mismatch
The reference declares `host: str = "0.0.0.0"` (web/web_service.py:543). The
port declared `host?: string` and resolved the value in the body with
`host ?? '0.0.0.0'` — same runtime behaviour, wrong signature. Moved the
initializer onto the parameter.
AgentBase.onSummary(rawData) — required-flip
AgentBase.onFunctionCall(rawData) — required-flip
Both are override hooks the reference declares with `raw_data=None`
(core/agent_base.py:511, core/mixins/tool_mixin.py:235), so a caller may
invoke — and a subclass may override — without it. The port required it.
Made both optional.
AIChatClient.summarize(options) — default-mismatch
The reference's `summary_prompt` defaults to None; the port's options bag
carried an invented `= {}` initializer. Dropped it (`options?:`) and made the
member reads optional-chained.
PromptManager.defineContexts(contexts) — required-flip + default-invented
The reference's PromptManager.define_contexts(contexts) REQUIRES the argument
and stores it (core/agent/prompt/manager.py:79); get_contexts() reads it back
(:309). TS's PromptManager had NEITHER method — the enumerator's mixin
projection pointed both names at AgentBase.defineContexts, whose optional-arg
builder-entry signature is a different method, so the projection clobbered the
contract with a required-flip. Implemented both for real on PromptManager
(including the reference's reject-a-non-object branch) and dropped the two
names from the projection list so the real signatures are enumerated.
Also wired the delegation the reference performs: PromptMixin.define_contexts
forwards a supplied value to `self._prompt_manager.define_contexts(contexts)`
(core/mixins/prompt_mixin.py:149). TS's AgentBase.defineContexts only set its
own contextsBuilder, so the manager's store was never populated and the new
reader would have been permanently null.
tests/ParamDefaults.test.ts pins all of it. Every test exercises the parameter
by OMITTING it — a test that always passes the argument explicitly proves
nothing about a default. The two required-flips are pinned at TYPE-CHECK time
(the call sites do not compile if the param goes back to required), and
`defineContexts()` with no argument is pinned with `@ts-expect-error`, which
fails as an UNUSED directive if a default is ever reintroduced.
Each of the three new drift kinds was mutation-tested: reverting the fix turns
the gate RED (and, for the required-flips, fails tsc), restoring it turns it
green.
DRIFT 6 -> 0.
An overloaded TS method is spelled as N bodyless signature declarations followed
by one implementation declaration that has a body — all separate `cls.members`
entries sharing a name. The enumerator recorded the FIRST of them, so it saw only
the narrowest overload: `getBasicAuthCredentials` read as `(includeSource?) ->
[string,string]` when the method actually returns the `[string,string] |
[string,string,source]` union the Python reference records, and
`setSessionMetadata` read as 2 params returning void when the 3-param
`(sessionId, key, value) -> boolean` parity form is right there.
Record the IMPLEMENTATION declaration instead. TS requires it to be compatible
with every overload, so it carries the widened param types and the union return
by construction — the same shape a single polymorphic Python `def` records.
Separately, the general options-object unfold guarded on `ts.isTypeLiteralNode`,
which is false for an INTERSECTION bag (`{ event?: string } & Record<string,
unknown>` — TS's spelling of `def m(self, *, event=None, **kwargs)`).
`Call.user_event` was on the unfold allowlist but never unfolded because of it.
Resolve the literal arm through the intersection so it does.
Also give `getBasicAuthCredentials` the reference's `= false` default (behaviour
is unchanged; `if (includeSource)` already treated undefined and false alike),
and dedupe union arms that canonicalise to the same string — a string-literal
union like `'provided' | 'environment' | 'generated'` was recorded as
`union<string,string,string>`, a 3-arm union describing one type. The diff's own
normaliser already deduped at compare time, so this only ever made the stored
artifact noisier than the surface it describes; the finding sets are identical
before and after.
Retires 3 PORT_SIGNATURE_OMISSIONS entries (206 -> 204). Excused findings
479 -> 473 with drift flat at 300 and no new drift.
Retiring the `set_session_metadata` entry revealed a genuine divergence the
omission had been hiding: because the 2-arg bulk overload is a strict PREFIX of
the 3-arg one, `value` is optional in the union where the reference requires it.
That is real surface, not an enumerator limit — the entry stays, with a rationale
that now describes what the code actually does, pending a ruling on the bulk
overload (which tests and docs both use).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…lementation 202d79f recorded an overloaded method's IMPLEMENTATION declaration. That over- corrected. TS requires the implementation signature to be compatible with every overload, which makes it strictly LOOSER than anything a caller can invoke — it is compiler plumbing, not surface. `setSessionMetadata` is the case that shows it. It declares setSessionMetadata(sessionId, metadataOrKey: Record<string,unknown>): void; setSessionMetadata(sessionId, key: string, value: unknown): boolean; The second overload matches the reference exactly — `set_session_metadata( call_id, key, value) -> bool`, all three required. But the implementation must write `value?` purely because the sibling 2-arg form takes no third argument, so recording it reported a `required-flip` for a state no caller can reach: the 2-arg overload does not accept a third argument, and the 3-arg overload requires it. The type system already enforces the reference's contract. Reconcile across the DECLARED overloads instead, restricted to PREFIX-overloads (java's `optional_param_names()` rule, commit ea7e0ba): the arities must be strictly increasing, so every param keeps its position across the set and the longest declaration carries them all with the required-ness each was actually declared with. Per-position TYPES may differ — that is what overloading is for. Same-arity declarations are a polymorphic dispatch rather than a growing prefix, with no single representative, so those still fall back to the implementation's honest union. Return types are unioned over the whole declared set independently, since the param list comes from one declaration but the return genuinely is the union of every overload's. `set_session_metadata` now records (self, session_id, key, value) all required returning union<void,bool> — comparing EQUAL to the reference. No source change, and the TS-native bulk overload and its 8 test call sites are untouched. Retires the 4th and last entry of the overload section (PORT_SIGNATURE_OMISSIONS 204 -> 203; the section is now empty and removed). Excused 473 -> 471 with ZERO new findings; the drift set is byte-identical to the pre-campaign baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…ot the ledger
Wave B: an omission stops the drift checker comparing a symbol forever, so a
divergence excused as "we spell it differently" / "language X has no analog" is a
blind spot, not a judgement call. Six of TypeScript's seven such entries are
reconciled here in the type/module tables so comparison KEEPS RUNNING. The
seventh is a real capability gap and is reported, not laundered.
Measured, as SETS (pristine HEAD -> here):
drift 300 -> 300 (zero new findings, zero resolved)
excused 471 -> 459 (13 divergences now compare EQUAL; net -12)
omission entries 203 -> 197
Three of the retired rationales described behaviour the source does not have.
1. `RegExp` was mapped to `string` in type_aliases.yaml. It is a compiled-pattern
OBJECT, and the reference records Python's `re.Pattern` as `class:Pattern`.
Because a union dedupes, `string | RegExp` collapsed to a single `string` arm,
erasing the RegExp affordance from the recorded surface and forcing an
omission on DataMap.expression. ruby and perl already emit
`union<string,class:Pattern>` and carry no omission. Now TS does too.
2. "The FastAPI `Request` has no Hono/TS analog" was false. Hono's per-request
`Context` is that analog, and go already maps its `http.Request` and compares
equal (the differ matches bare `class:` refs by leaf name). `Context` now maps
to `class:Request`; it appears in exactly one audited public signature.
3. "TS types payloads as named shapes where Python uses dict" was a real
divergence with the wrong cause. The diff checker has ALWAYS held a
spec-generated TypedDict compatible with `dict<string,any>` in either
direction. The rule never fired because this port's SIGNATURE enumerator was
missing the `src/PlatformContracts.generated.ts` module alias that its own
SURFACE enumerator already had, so `SwmlRequestData` fell back to
`signalwire.platform_contracts.generated.*` — a path carrying none of the
generator-module markers `normalize_type` keys on — and never normalised to
the `gen:<Name>` token. Both that file and the reference's
`swml_webhooks_types_generated.py` are generated from the SAME spec
(rest-apis/swml-webhooks/openapi.yaml), so they must record the same module.
Adding the one alias retired 8 excuses beyond the entries in scope.
Two REAL port defects the omissions were hiding, both fixed:
* `InfoGathererAgent.onSwmlRequest` was a LIVE override narrowed to one
parameter, so neither the callback path nor the framework request object could
reach it or any subclass — the reference passes both at its call sites. Widened
to the base hook's full `(requestData, callbackPath, context)`.
* `AgentBase.onSwmlRequest` declared its first parameter REQUIRED where the
reference defaults it to None (a `required` flip is contract drift, not idiom).
Left UNRESOLVED for an owner ruling, with the false rationale corrected in place:
`WebService.security` returns `SslConfig` (SSL/HSTS only) where the reference
returns the unified `SecurityConfig` (SSL + CORS + security headers + allowed
hosts + basic auth), so 4 capability groups are unreachable through the accessor.
This is a NARROWING, not the "config class-name spelling" difference the old
rationale claimed: TS's own faithful `SecurityConfig` already exists in
src/SWMLService.ts — `WebService` just does not use it. Closing it is a
behavioural change to a security-relevant surface, so it is not made unilaterally.
Verified: bash scripts/run-ci.sh -> exit 0 (SURFACE/TEST/LINT/BEHAVIORAL all PASS).
port_signatures.json regenerated AFTER run-ci (the surface suite's _restore_tree
reverts it) and its committed content checked, not just its diff status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…ise was false All 43 `ts-options-object` signature omissions shared one rationale: TS collapses the reference's positional-or-keyword params into an options bag, "so the param KIND reads as a mismatch." That premise is false. `diff_port_signatures.py::compare_param_properties` folds ref=`positional` -> port=`keyword` (:917, directional by design — a Python positional-or-keyword param IS callable by name, so a port spelling it as a named field preserves the exact capability). The fold predates none of these entries; they were simply never re-audited against it. So these methods can UNFOLD and be COMPARED rather than excused. Promoting them exposed a second, real enumerator defect: the unfold recorded `default: null` for every optional member, which ASSERTS "the default is null" — false wherever the reference declares a real one, manufacturing 44 spurious `default-mismatch` findings against a port that behaves identically (verified in source: PomSection's ctor applies `?? ''` / `?? false`; FunctionResult. joinConference omits each key when it equals the reference default). A TS optional property carries no syntactic default — the value is applied downstream in the body or a callee's constructor — so the declaration site cannot supply it. Omit the key instead: the differ then counts it as UNRECORDED (direction (b)) rather than drift, which is the truthful classification. `required` still compares, so an optional->required flip is still caught. Also keyed 8 entries by their SOURCE class (`agent_base.AgentBase.*`) rather than the mixin-projected canonical name — the unfold runs at enumeration time, before MIXIN_PROJECTIONS moves the computed signature, so a projected key never fired. Measured (pinned oracle, --numeric-monotype as the gate runs it): drift 200 -> 200 (set IDENTICAL: 0 resolved, 0 introduced) excused 435 -> 409 (-26) omission entries 197 -> 168; ts-options-object 43 -> 8 params compared 1259 -> 1428 (+169 now genuinely compared) param-property drift stays ZERO Ten entries could NOT be retired and keep a corrected rationale, because the mechanism genuinely does not reach them (each verified in source, not from the old text): a NAMED interface rather than an inline type literal (add_language, add_pronunciation, build_config, SkillBase.define_tool, load_skill); a generic INTERSECTION over a mapped `Omit<...>` (ToolRegistry/ToolMixin.define_tool); a genuine object argument, not a bag (SkillMixin.add_skill); flat optional positionals (AgentServer.run); and WebMixin.run, whose bag members are declared in a different ORDER than the reference's positional list — the differ compares by position, so unfolding it pairs event<->host and manufactures four mismatches out of correctly-corresponding members. DataMap.webhook unfolds (its `opts` members are read off individually onto separate wire keys); DataMap.foreach stays out, as the boundary comment requires — it takes ONE required param passed through whole (`webhook['foreach'] = config`), and its members are already snake_case because they ARE the wire shape. Default coverage drops 99.5% -> 78.3% (308 unrecorded) as a direct consequence of not asserting defaults the port cannot see. That is 308 honest "unknown"s replacing 44 false assertions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
porting-sdk dcff742 turned BasicCredentials/BearerCredentials from DANGLING oracle
refs into REAL classes. Before it, griffe could not resolve FastAPI's names into the
signalwire. tree, so the oracle named a class with no definition anywhere — and the
gate still compared it by leaf name. A port carrying the real shape drifted against a
phantom and a port carrying nothing also drifted; neither could win. This port carried
nothing, and papered over the gap with two excuses.
Both excuses were wrong on the same point: they claimed FastAPI's wrapper types "have
no Hono/TS analog". FastAPI was never the contract. Those objects are just the fields
parsed out of the Authorization header, and that shape is entirely portable — cpp,
ruby and perl had each independently hand-rolled the same two carriers with the same
field names. So the fix is to CARRY the contract, not to keep excusing it:
src/AuthHandler.ts now exports two carriers matching the oracle exactly
BasicCredentials { username, password }
BearerCredentials { scheme, credentials }
verifyBasicAuth / verifyBearerToken take them, matching the reference's
verify_basic_auth(credentials) / verify_bearer_token(credentials).
The carriers are LOAD-BEARING, not decorative: validate() now parses the Authorization
header into a carrier and calls the verify method, so the one comparison path is the
one the parity gate checks. verifyBearerToken compares only `credentials`, the same
field the reference compares — `scheme` is carried for header fidelity and is not part
of the secret comparison. Both methods had ZERO callers and ZERO tests before this, so
changing their shape broke nothing; 6 tests now cover the fields, the not-configured
short-circuits, and the validate() round trip.
Retired, not re-excused, in PORT_SIGNATURE_OMISSIONS.md:
- AuthHandler.verify_basic_auth ("TS takes the unwrapped (username, password) pair")
- AuthHandler.verify_bearer_token ("TS takes the already-unwrapped token: string")
- the stale framework/typed-shape bullet still asserting the wrapper types are omitted
Measured as SETS, both arms against a scratchpad-PINNED oracle whose md5 was asserted
identical across the two runs (python_signatures 7cb4b078…, python_surface d15aced4…),
with the flags this port's gate actually passes (--omissions on both sides,
--numeric-monotype):
DRIFT 4 hard -> 0 4 resolved, 0 INTRODUCED
BasicCredentials.username/.password, BearerCredentials.scheme/.credentials
(all missing-port)
excused 584 -> 580 4 resolved, 0 INTRODUCED
verify_basic_auth param-count-mismatch, verify_bearer_token param-mismatch,
and both classes' construction-missing-class
SURFACE 6 missing -> 0 6 resolved, 0 INTRODUCED
excused_additions 431 -> 431 UNCHANGED, excused_omissions 34 -> 34 UNCHANGED
Nothing outside the credential surface moved in either direction.
Artifact staleness, checked before starting (ruby's c36ddd0 defect): port_signatures.json
regenerated byte-identical to git show HEAD: — not stale. port_surface.json differed from
HEAD in exactly one leaf, the `generated_from` provenance SHA, with zero surface content
drift; the previous commit stamped its parent. Both are regenerated and committed here.
SEMVER-DIFF reports verifyBasicAuth as a breaking param-count change against the last
release. That is accurate and intended — it is the point of the change — and the gate is
--report-only, so no allow-list entry was added.
Coordinated-With: porting-sdk dcff742 (oracle change; pin via PORTING_SDK_REF per
COORDINATED_PASS.md). Workflow audit: all 8 CI-path porting-sdk checkouts already use
`ref: ${{ vars.PORTING_SDK_REF || 'main' }}`. The one literal `ref: main` is in
publish.yml and is a DELIBERATE, documented exception — a release must never ship from
an unmerged wave branch — not the leftover the coordinated pass is looking for.
Verification (real exit codes captured to a file, not read off the harness — it
misreported this run's exit as 0 when it was 1):
bash scripts/run-ci.sh -> 1, gates: SURFACE only
SURFACE:SIGNATURES PASS SURFACE:DRIFT PASS SURFACE:SEMVER-DIFF PASS
SURFACE:GEN-TYPE-DEGENERACY PASS SURFACE:GEN-IDIOM PASS
SURFACE-FRESH + SURFACE-DIFF FAIL — both because the regenerated artifacts were
still UNCOMMITTED: SURFACE-FRESH compares a fresh regen against the COMMITTED
blob, and surface.py:_restore_tree() git-checkouts the artifacts after the suite.
This commit is that missing step; the other 28 gates all PASS.
scripts/run-tests.sh AuthHandler -> 0, 24 passed (18 before)
scripts/run-lint.sh -> 0
diff_port_signatures.py (pinned oracle, --numeric-monotype, --omissions) -> 0
signatures match (1561 reference symbols, 1893 port symbols, 605 excused)
diff_port_surface.py (pinned oracle, --omissions --additions) -> 0
port matches Python reference (2709 symbols; 36 excused omissions, 468 excused additions)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…r the __init__ oracle fix
The three `PORT_SIGNATURE_OMISSIONS.md` entries for `Call.on`, `RelayClient.on_call`
and `RelayClient.on_message` all justified themselves with the same claim: that
Python "wraps" the handler in an `EventHandler` / `CallHandler` / `MessageHandler`
*class*, and that TS instead types the handler as a plain callback returning void.
Both halves of that claim are false. In the reference these are type ALIASES, not
classes:
signalwire/relay/call.py:56
EventHandler = Callable[[RelayEvent], Coroutine[Any, Any, None] | None]
signalwire/relay/client.py:74-75
CallHandler = Callable[["Call"], Coroutine[Any, Any, None]]
MessageHandler = Callable[["Message"], Coroutine[Any, Any, None]]
There is no wrapper class to be "internal" to. And TS does not return void: it
declares the same three aliases in src/relay/types.ts:60-66 and
`RelayClient.onCall`/`onMessage` return the handler for decorator-style use,
exactly as the reference does. The two implementations are structural mirrors.
The entries were only ever paying for oracle bug 85e12c7, which emitted these
aliases as dangling `class:` refs naming classes defined nowhere. With that fixed
the oracle records the callable type the alias denotes, and the port's recorded
signatures are now byte-identical to the reference's — so the omissions suppress
nothing. A/B against a pinned oracle (python_signatures.json md5
7d9f2fa2e1f385df2afac88b657a6475), `--omissions` on both sides plus
`--numeric-monotype`: 0 drift / 603 excused WITH the entries and 0 drift / 603
excused WITHOUT, the differ's full output byte-for-byte identical. Dead entries,
deleted rather than re-tagged.
port_surface.json is regenerated for oracle fix 8828dd2, which taught the surface
oracle to record a synthesized `__init__`. The delta is exactly the 30 classes
that fix named — 19 relay events, 3 ai_chat carriers, RequestOptions and the 2
credential carriers — 30 added, 0 removed, every one an `__init__`. SURFACE-DIFF
goes from 30 unexcused missing symbols to clean without a single ledger entry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
Owner ruling 2026-07-28: the whole port fleet lands on a single unreleased 3.0.0. This port was at 3.2.0; nothing in the 3.x range was ever published (typescript's published tags top out at v2.0.5), so the downgrade regresses no artifact. The credential-carrier work removed the public 2-arg verifyBasicAuth(username, password) — genuinely breaking — and the fleet absorbs that into one coordinated 3.0.0 rather than staggering majors. Declaration site: package.json "version". package-lock.json self-references the package's own version in both its root entry and packages[""], so both are updated in lockstep. The REST User-Agent is derived at runtime from package.json (buildUserAgent in src/rest/HttpClient.ts) and needs no edit. The CHANGELOG's "## 3.2.0" heading is a historical release entry, not a declaration site, and is left as-is. This sets version INTENT only. No tag, no release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…ce payload
Per the owner ruling (2026-07-28) every port's release floor becomes 3.0.0.
`port_signatures.baseline.json` gets `baseline_version = 3.0.0` AND its recorded
surface payload replaced with the current enumeration.
Both halves are required. SEMVER-DIFF does not merely compare version numbers —
it diffs the surface against the floor's recorded `modules` payload, so bumping
`baseline_version` alone leaves `required = 'major'` against a floor whose payload
still describes an older surface. Before this change the floor reported:
3.0.2 (3.0.2) -> 3.0.0 actual bump = 'downgrade', required = 'major' [MISMATCH]
BREAKING — 15 member(s) removed since last release
After the payload swap all 15 breaking removals are gone:
3.0.0 (3.0.0) -> 3.0.0 (package.json) actual bump = 'none'
no public surface change since last release.
This is a PAYLOAD SWAP, not a file copy: the floor carries release-anchor
metadata the current artifact does not. `modules` + `construction` come from a
FRESH `npx tsx scripts/enumerate-signatures.ts` (94 -> 98 modules; the regen was
byte-identical to the committed port_signatures.json, confirming that artifact was
already current); `baseline_version` is set to 3.0.0; and the provenance anchor
(`generated_from_tag` / `tag_sha` / `generated_from_commit`) is re-pointed from
the stale 80381af to this branch's HEAD fc00922. No v3.0.0 tag exists yet, so a
bare 40-hex commit sha is the correct anchor — the same commit-anchored
convention the rust floor already uses, and one semver_diff accepts
unconditionally (_SHA_RE, semver_diff.py:169/176).
Semantics: the floor stops being "the surface as last published" and becomes
"the surface as of the 3.0.0 wave". That is coherent because nothing 3.x/4.x
ever shipped. SEMVER-DIFF will no longer flag anything already in today's
surface; future breaking changes are still caught, measured against the new floor.
KNOWN, PRE-EXISTING, NOT INTRODUCED HERE: run standalone (without --report-only)
semver_diff still exits 1 with `required = 'patch'` despite reporting "no public
surface change". That is a defect in the checker, not in this floor:
semver_diff.py:495 fires on `if allow:` — the mere EXISTENCE of a non-empty
SEMVER_DIFF_ALLOW.md — and overwrites an already-correct `required = 'none'` with
'patch', even when none of the allowlisted symbols appear in the diff. This port's
2 allowlist entries (CrudResource.get/list) are pre-existing and unrelated to the
current diff. run-ci is unaffected: it invokes SEMVER-DIFF with --report-only
(the D5 "re-anchor at cut" wave setting), which exits 0. Reported for the owner
rather than papered over; no allowlist entry was added.
run-ci: real exit 0, 23 gates PASS / 0 FAIL — UNCHANGED from the pre-change
baseline. No gate moved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
The inbound `Authorization` guards compared the scheme token with a
case-sensitive `startsWith('Bearer ')` / `startsWith('Basic ')`. RFC 7235
makes the auth-scheme token case-insensitive, and the reference
(FastAPI `HTTPBearer` / `HTTPBasic`) partitions the header on the FIRST
space and compares `scheme.lower() != "bearer"` / `!= "basic"`. So a
legal `authorization: bearer <token>` authenticated against the
reference and was 401'd here.
Fixed at all four inbound comparison sites, via a `schemeParam()` helper
that mirrors `get_authorization_scheme_param` (partition on the first
space, strip the credential, case-insensitive scheme compare):
src/AuthHandler.ts Bearer branch
src/AuthHandler.ts Basic branch
src/SWMLService.ts _checkBasicAuthHeaders
src/AgentBase.ts checkAgentBasicAuth
Outbound emitters that BUILD a canonical-case `Basic `/`Bearer ` header
(HttpClient, AIChatClient, swaig-test, the skills) are unchanged —
emitting the canonical case is correct.
The Bearer branch now carries the scheme through to `BearerCredentials`
exactly as the client sent it, matching the reference, which reports the
wire scheme rather than a canonicalized one.
Colon-handling was already correct in all three Basic-decoding paths and
is now covered by tests: the reference does
`username, separator, password = data.partition(":")` and rejects when
there is no separator, so a colon-less payload must never authenticate as
a user with an empty password.
Tests assert both directions — lowercase/mixed-case `bearer`/`basic` are
accepted, while `Digest`, `Negotiate`, `Basicx`/`basicx`,
`Bearer`-on-the-Basic-branch, a scheme-less header, and a colon-less
Basic payload all stay rejected. The 8 accept assertions fail against the
unfixed guards; every rejection assertion passes both before and after.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
Per the owner's 2026-07-28 ruling, every CHANGELOG heading above a port's last genuinely published tag collapses into a single unreleased 3.0.0 entry. The TypeScript port's published tags top out at v2.0.5 (verified against `git ls-remote --tags origin`, not just local tags), so the 3.2.0 / 3.1.0 / 3.0.2 headings were all unreleased drafts -- no released artifact regresses. They are consolidated into one `## 3.0.0` entry with NO content dropped: all nine bullets survive, regrouped under Added / Fixed / Notes. The `## Unreleased (Wave 1)` section above them is untouched -- it carries no version number, so it is not the "top entry" META-CONSISTENT resolves. package.json's version was already 3.0.0; this is the static mirror that META-CONSISTENT cross-checks against it: meta_consistent.py --port typescript exit 1 -> exit 0 (was: manifest version '3.0.0' != top CHANGELOG entry '3.2.0') This sets version INTENT only -- no tag, no release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
Owner ruling 2026-07-28. DOCUMENTATION ONLY: no gate reads this file and nothing fails on its presence. It records why the version looks the way it does, so the next session does not re-derive it or casually bump one — and it carries the delete-before-release checklist in its own body. Nothing 3.x/4.x was ever published (git ls-remote tops out at v1.1.2 for rust/dotnet, v2.0.x for most others), so the freeze rewrote no real history. Exempted in porting-sdk root_hygiene.py 287b7f2.
…re TOKEN-INTEROP
Node's 'base64url' encoding STRIPS the '=' padding. The reference mints with
base64.urlsafe_b64encode, which keeps it, and validates with urlsafe_b64decode,
which RAISES on a stripped '=' — so every token this port minted was unusable to
the reference and to any port that decodes strictly, even with a correct key and a
correct HMAC. In production every secure tool call fails authentication.
Now: standard base64, then the urlsafe alphabet, padding intact.
Also wires the TOKEN-INTEROP gate (property 3 of the SWAIG tool-token contract: a
token this port MINTS validates under the REFERENCE's own decoder). SECURE-DEFAULT
proves a token is minted and the fleet keying check proves the HMAC key; NEITHER
sees the base64 ENVELOPE, so a port can ship correct-key correct-HMAC tokens that no
other implementation accepts. Per-PR rather than nightly — a security property
should not wait.
Seven of the ten ports shipped an unpadded envelope, invisible to each port's own
tests because every port's DECODER tolerates missing padding while the reference's
urlsafe_b64decode RAISES on it — so round-tripping against ourselves could never
catch it. That is why the gate validates against the reference's decoder.
Verified: TOKEN-INTEROP exit 0; reverting to `toString('base64url')` reproduces "urlsafe_b64decode raised Error('Incorrect padding')", so the gate fails for the right reason. vitest 2882/2882.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…differ
porting-sdk 7034c33 stopped TYPE-EROSION from counting a MISALIGNED slot as an erased
type. The gate keyed on position, which is only meaningful while both param lists
describe the same parameters; where a port's list has a different SHAPE (different
arity, or a variadic catch-all standing in for a named param) index i was a different
parameter on each side, and an `any` there was reported as an erased type. Those methods
are already reported — correctly — by diff_port_signatures as param-count-mismatch.
So this port's old ratchet banked a number that was part real erosion and part
double-billed count-mismatch. Re-baselined onto what the corrected differ measures.
ratchet 45 -> 5 (the delta is measurement correction, not a surface change)
No port code changed and no erosion was fixed by this commit: the number moves because
the MEASUREMENT was corrected, not because the surface improved. The ratchet doctrine is
unchanged — drive it DOWN, never up — and it now ratchets against a number that means
one thing.
Fleet-wide the same correction takes 524 -> 257; 292 of the 524 were the artifact. The
skip is never silent: each run prints how many methods went unmeasured and names the
gate that owns them.
Verified: diff_port_type_erosion.py --port typescript --repo . --max 5 -> exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…ST the wiring
typescript is the ONE port that already had this right: `Logger.log` calls
`stripControlChars(serialized)` before merging (Logger.ts:339), so the scrub was
genuinely on the emission path. rust, cpp and java each shipped the same function
with ZERO call sites — public, correct, and protecting nothing. This commit does
not change ts behaviour; it closes the two gaps around it.
1. PARAM NAME. The reference's public contract is `strip_control_chars(event_dict)`
(logging_config.py:33); this port recorded `data`. Renamed the parameter to
`eventDict` — the enumerator's camelCase->snake_case fold turns that into
`event_dict`, so the recorded signature now matches the oracle with NO
rename-table entry needed. Verified by regenerating port_signatures.json:
signalwire.core.logging_config [('event_dict', 'any')]
The alternative was a free-function param-rename table, which does not exist in
enumerate-signatures.ts (its rename tables are keyed by CLASS). Renaming in the
port source is the smaller, more honest change: nothing about the reference is
being reconciled away, the port simply says what it means. All three call sites
are positional, so nothing else moved.
2. NO TEST COVERED THE WIRING. The protection was real but unguarded — the exact
state in which rust/cpp/java's scrub rotted into a no-op without any gate
noticing. The new test reads what the logger ACTUALLY emitted (via the existing
console spies) rather than calling stripControlChars directly, so it fails when
the wiring is removed. Verified by deleting the call from Logger.log — RED:
× strips control characters from emitted log data
Tests 1 failed | 48 passed (49)
A helper-only test passes against that same break.
The second test asserts tab/newline/CR SURVIVE: a scrub that ate them would
satisfy "no control chars" while mangling every multi-line message.
Verified: run-tests.sh -> exit 0, 136 files / 2884 tests (2882 before; +2 new).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
… doc comment 65a1a1c renamed the parameter to `eventDict` and explained WHY in the shipped doc comment — naming the enumerator, the camelCase->snake_case fold, the rename table, and "the oracle". The nightly PUBLIC-JARGON gate caught the last of those: src/Logger.ts:237: banned public-doc jargon 'oracle' [public-jargon] typescript: 1 jargon leak(s) in 1 file(s) It is a nightly-tier gate, which is why the per-PR run-ci was green locally. The gate is right, and not only about the one banned word. An SDK consumer reading `stripControlChars` in their editor does not have a porting matrix, an enumerator, or a reference oracle; every sentence of that paragraph described OUR process, not what the parameter is or does. That reasoning belongs in the commit that made the decision (65a1a1c, where it still is), not in shipped API documentation. The parameter description is now what a caller needs: the log event record whose string values get sanitized. Nothing about the behaviour changed. Verified: run-ci.sh --rules PUBLIC-JARGON -> PASS. run-tests.sh -> 2884 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…ionales
Part of the fleet-wide false-ledger sweep. Drift-neutral: excused divergences 602 -> 602,
SURFACE 7/7 PASS before and after — the proof that the 10 deleted entries were suppressing
nothing at all.
THREE CLAIMS IN MY OWN BRIEF DID NOT SURVIVE SOURCE:
- "3 EventHandler entries" — `grep -c EventHandler PORT_*.md` returns 0 in BOTH ledgers.
Whatever task #134 measured is not in this tree.
- "the differ tolerates `device: Device` as a superset today" — implying a real shape
difference underneath. Run with an EMPTY omissions file, the differ reports no device
divergence at all. There was nothing for those entries to excuse.
- "~22 entries" understated it. All 164 signature entries were probed MECHANICALLY (remove one
line, ask the differ what it then says about that symbol): 38 suppress nothing, and the
largest false-rationale cluster was a whole section the suggested grep never matched.
DELETED (10) — each verified against reference source, not against the rationale's own story:
4x .device claimed "Python sets it dynamically". relay/event.py:41,65,238 declare
`device: dict[str, Any] = field(default_factory=dict)` — STATIC dataclass
fields, and the oracle records all four.
strip_control_chars claimed the 3-arg structlog processor signature. Reference is
core/logging_config.py:33 `def strip_control_chars(event_dict)` — one
parameter, deliberately refactored (the _as_processor docstring says so).
FIXED-BUT-NOT-DELISTED, the same mode found in perl.
SignalWireRestError.body claimed "Python has no such attribute". rest/_base.py:88 is
`self.body = body`.
4x typed getters SkillBase.agent, FAQBotAgent.faqs, SurveyAgent.questions, Action.call each
claimed "the signatures oracle does not record" it. It records all four.
REWORDED AND KEPT (9) — THE TRAP THAT GOT GO, CAUGHT HERE BY PROBE.
The whole `## Idiom: TS fluent API returns this` section (8 entries) claims each method returns
`this`. NOT ONE DOES — getAgents(): Map<...>, createPaymentAction(): PaymentAction,
getSection(): PomSection | undefined, toDict(): PomSectionData[], debugToken(): DebugTokenResult,
getPromptSections(): SkillPromptSection[]. On the false-rationale finding alone that section
looked deletable. But all 8 suppress REAL divergences (7 return-type mismatches, 1 missing-port),
so deleting them would have red'd ts's nightly exactly as happened in go (85a2f72). Wording
corrected, entries kept — MISWORDED-BUT-REAL.
Ninth: clear_digit_bindings claimed a missing **kwargs passthrough. The reference does have one,
but the oracle strips kwargs tails; the divergence actually suppressed is `realm`'s kind,
keyword vs positional.
Deleted outright rather than commented — a tombstone keeps the symbol name and the stale claim
greppable, so an audit of "which symbols are excused" gets a false positive.
Verification:
sw-verify typescript --gates SURFACE,LEDGER -> exit 0
SURFACE PASS · LEDGER PASS
NO-LAUNDER PASS (impossible: 28 · approved: 7 · idiom: 0 · unclassified: 0 · banned: 0)
Drift-neutrality established by per-entry probe against a baseline, not by an after-state run.
run-format.sh exit 0 and --check clean; it changed nothing beyond this ledger.
Edit re-confirmed present AFTER the gate run, per the #112 run-ci-checkout trap.
PORT_OMISSIONS.md audited, unchanged. NO TypeScript source touched.
TWO THINGS FOR AN OWNER, NOT DONE HERE:
- WebMixin.run carries a stale MECHANISM narrative: it describes the differ unfolding the
options bag and pairing event<->host, but the differ actually reports a plain
`param-count-mismatch: reference has 5, port has 1`. Its FACTS (member order, return type)
are true and verified at src/AgentBase.ts:3114, so rewriting it was out of scope.
- diff_port_signatures.py HAS NO STALE-ENTRY DETECTION. An entry naming a symbol with zero
divergence is silently ignored — which is precisely why these 10 survived. A "ledger entry
that excuses nothing" check would make this whole campaign self-policing fleet-wide, instead
of requiring a per-port manual sweep.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…dded to the dump
ENVELOPE was DEFINED for typescript by porting-sdk and scheduled by only ONE port in the fleet (go).
The standalone `ENVELOPE` token elsewhere in this file was a COMMENT, not a rule. So the behavioural
request-options contract went unchecked here.
BURNED TO ZERO BEFORE WIRING, per the standing rule.
BEFORE: (3 case(s) pending dump coverage: compose_ctx_timeout_alone, compose_abort_signal_alone,
compose_ctx_and_signal_both)
✗ 3 FAIL — typescript
AFTER: ✓ PASS — typescript
THE REDS WERE MISSING DUMP CASES, NOT MISSING SDK BEHAVIOUR. An earlier audit called them
"unimplemented request-options behavior"; that was wrong and briefing it that way would have sent
someone hunting a bug that does not exist. scripts/envelope-dump.ts emitted four `envelope_*` cases
and zero `compose_*` cases. The SDK was fine all along.
WHAT THE COMPOSE CASES ACTUALLY PROVE. The corpus arms a slow 3s 200; the differ (_drive_compose,
diff_port_envelope.py:211-277) drives three legs and reduces each to one boolean. The timing
constants are a CONTRACT with the differ, not tunable knobs — they are the separation that makes
each leg attribute its cancellation to exactly ONE source:
1500ms window — the sole observable; sits between the 500ms short timeout (fires well inside)
and 3000ms natural completion (lands well outside), so a dropped cancellation
source cannot sneak in.
500ms short — < 3s, so it fires. Used by the `ctx` and `both` legs.
10s long — > 3s, so it CANNOT fire; that is what makes the `signal` leg prove the SIGNAL
did the cutting rather than a timeout.
`both` — arms the short timeout AND a live-but-untriggered signal, so the timeout fires
IN THE PRESENCE OF a signal. This reds a port that REPLACES the timeout with the
signal instead of MERGING them — the go GO-5 bug at rest/client.go:327-330.
TYPESCRIPT IS STRUCTURALLY IMMUNE TO THAT BUG, and I verified the mechanism rather than the
symptom: HttpClient.ts:285-291 `_attemptSignal` returns
`AbortSignal.any([opts.abortSignal, timeoutSignal])` — a genuine merge. The earlier note citing
:205-213 was pointing at the pre-attempt cooperative check, which is real but is not the line these
cases exercise.
An idiom note worth recording: TS has no context.Context, so the "ctx" source here is
RequestOptions.timeout (RequestOptions.ts:40) — which is exactly what the PYTHON ORACLE uses too.
TS maps to the oracle more directly than go does; go had to reach for GetContext plus a
client-default abortSignal.
Compose cases drive a local node:http server that sleeps 3s rather than arming a delay_ms scenario
on the shared mock — mirroring go's httptest approach and keeping these legs off the shared mock
entirely. The artifact is booleans only, so the oracle comparison is unaffected.
TIER: BLOCKING (per-PR). ENVELOPE joins the existing BEHAVIORAL suite line, so it costs no extra
gate invocation. The desc string is updated alongside the --rules list — a desc that enumerates
rules it does not schedule reads as coverage and is worse than no desc.
Verification:
ENVELOPE alone, run three times consecutively: PASS each time (the compose legs are not
timing-lucky).
FULL BEHAVIORAL SUITE as run-ci now invokes it, minus SWAIG-HTTP-INVOKE: all 21 rules PASS.
scripts/run-tests.sh exit 0 — 136 files, 2884 tests passed
scripts/run-format.sh exit 0 — Prettier clean (the dump was reformatted BEFORE the passing runs
above, so there is no formatting delta waiting to bite CI)
scripts/run-lint.sh exit 0 — tsc + eslint + check-ts-idioms clean
KNOWN SEPARATE RED, NOT CAUSED BY THIS COMMIT AND NOT FIXED HERE: SWAIG-HTTP-INVOKE now reports
"3 case(s) pending dump coverage: token_valid, token_forged, token_absent". That is fallout from
porting-sdk 04e24f2, which added inbound-token fixtures to the shared corpus; ts and go both need
their swaig-http dumps extended to emit them. Tracked separately — see task #57.
Found by the fleet-wide gate-wiring audit (task #55); part of that item's ENVELOPE tier.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…fail-open finding, left RED
porting-sdk 04e24f2 extended swaig_http_corpus.py with a query-string dimension and three
token fixtures. Dump programs hardcode their fixtures per language, so until this commit
typescript reported "3 case(s) pending dump coverage: token_valid, token_forged, token_absent"
and SWAIG-HTTP-INVOKE was red for a reason that said nothing about this port. That red was
missing dump coverage, not a defect. This commit converts it into a real measurement.
RESULT — two go green, ONE IS A GENUINE DEFECT AND STAYS RED:
✓ token_valid {handler_invoked: true, refused: false}
✓ token_forged {handler_invoked: false, refused: true}
✗ token_absent oracle {handler_invoked: false, refused: true}
typescript {handler_invoked: true, refused: false}
The two greens are load-bearing: they prove this port DOES read `__token` off the query string
and DOES refuse a well-formed forgery on a secure function. So the remaining red is narrow and
specific — absent-token only — and cannot be dismissed as "the dump does not mint tokens".
WHY IT IS RED. src/AgentBase.ts:2864-2866:
const url = new URL(c.req.url);
const token = url.searchParams.get('__token') ?? url.searchParams.get('token');
if (token) {
The entire validation block sits inside that `if`, so omitting the parameter skips the check
and the secure tool runs. That was CORRECT PARITY when it was written — the reference had the
identical shape — and the comment above it says so explicitly, citing agent_base.py:1413-1445.
signalwire-python 7c2f253 then made a secure tool REQUIRE a token (owner-ruled), which moved
the golden and left this port on the old contract.
DELIBERATELY NOT FIXED HERE. The fail-open→fail-closed rollout is one owner decision spanning
this port and the six mint-only ports (go, java, php, ruby, perl, dotnet); go is already
visibly red on the same fixture. Fixing typescript alone would hide the shape of the fleet
problem behind a green. Two things are therefore untouched and both are known:
* the `if (token)` guard itself
* ITS DOCTRINE COMMENT at AgentBase.ts:2858-2863, which now asserts something FALSE — that
"a MISSING token does not by itself block dispatch" is "parity with the reference". It
cites pre-7c2f253 line numbers. Whoever implements the ruling must delete the comment,
not just the branch; a stale doctrine comment is how a fixed behaviour gets reverted later
by someone reading it as intent.
Verification — the whole suite as CI runs it, not the new cases in isolation:
BEHAVIORAL --port typescript, full --rules list -> 21 PASS / 1 FAIL (token_absent only);
ENVELOPE, wired in 23afe42, stays green.
run-tests.sh -> exit 0, 136 files / 2884 tests
format + lint clean
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Document the last 69 undocumented exported symbols, all of them in two files,
and ratchet the DOC-SURFACE floor from 77.8% to 100.0%.
src/relay/constants.ts (65 symbols) — every RELAY protocol constant now carries
per-symbol TSDoc saying what wire field it is and what it means, not just the
group comment it sat under. Notable content beyond the literal:
- AGENT_STRING / PROTOCOL_VERSION: the two `signalwire.connect` handshake
fields; AGENT_STRING is per-port by design (python sends
signalwire-agents-python/1.0), so the divergence is documented rather than
read as drift.
- END_REASON_NO_ANSWER: the camelCase `noAnswer` wire spelling is the
server's, and is inconsistent with the snake_case siblings — called out so
no port "fixes" it.
- SERVER_PING_TIMEOUT: documented as a LOGGING-ONLY watchdog. Verified in
RelayClient._resetServerPingTimeout: on expiry it logs at debug and does
nothing else. A dead peer is caught by the client ping loop reaching
CLIENT_PING_MAX_FAILURES, which is what forces the close.
- EXECUTE_QUEUE_MAX: the bound on requests buffered while disconnected;
exceeding it rejects with RelayError instead of growing the buffer.
- MESSAGE_STATE_SENT documented as NOT terminal (a delivery receipt may still
upgrade it), matching MESSAGE_TERMINAL_STATES.
- RECORD_STATE_NO_INPUT documented as a terminal success-shaped outcome
distinct from a failure.
src/rest/RequestOptions.ts (4 symbols) — the DEFAULT_* contract floor.
DEFAULT_RETRIES is documented as zero-by-design (retries are opt-in; the first
non-2xx raises), and DEFAULT_RETRY_ON_STATUS notes that membership in the set
is necessary but not sufficient: statusIsRetryable further restricts POST/PATCH
to 429/503 so a side effect is never replayed on 500/502/504.
Docs only — no code, signature, or behaviour change. Both files' diffs are
purely additive (zero removed lines), and the `^export ` declaration lines in
constants.ts are byte-identical to HEAD.
Verified: npm run build (exit 0) · scripts/run-lint.sh (exit 0) ·
scripts/run-tests.sh 136 files / 2884 tests passed · doc_surface.py 327/327 =
100.0% against the newly pinned 100.0 floor (exit 0).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…open guard
The gate ran on every one of these ports and could never fail: it passed --report-only, so a
doc regression printed a line and the run stayed green. That was correct at graduation, when
the floor was well below 100 and the point was visibility. It is wrong now — the port is at
100.0% and .doc_surface_floor is pinned there, so a newly-undocumented public symbol is a real
regression with a pinned number to prove it.
ALSO REMOVED THE SKIP-WITH-PASS GUARD, which was a second fail-open and the more dangerous of
the two. Each invocation was wrapped in
if [ -f "$1/scripts/doc_surface.py" ]; then ...; else echo "not on porting-sdk main yet — skip-pass"; fi
That existed because doc_surface.py once lived only on a porting-sdk plan branch. It is on the
pinned PORTING_SDK_REF today, so the branch is dead — and its behaviour is wrong on principle:
a MISSING gate script must fail the run, not pass it. A path typo or a bad checkout would have
silently disabled the gate with a reassuring green message.
The stale comment blocks went with them; they still described a report-only gate at a
graduation-era floor ("51.8% today", "92.0% today") that no longer exists.
Per-PR rather than nightly: a pure text scan with no build, free next to the language toolchain.
Verified per port against its pinned floor, exit 0, plus `bash -n` on the modified script.
Also verified the gate can FAIL — see porting-sdk 6712757, which fixes a tolerance that made a
100% floor unfailable. That fix is a prerequisite for this commit meaning anything.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…id, not merely ignored
Ripples signalwire-python f171ce3 (owner-ruled 2026-07-29: "if the server doesn't read
them, remove them"). The reference commit anticipated this pass explicitly: "each port's
own create_simple_api_tool equivalent carries the SAME body-forwarding bug, so the ports
must be touched regardless."
THREE INDEPENDENT SOURCES AGREE `body` ON A WEBHOOK IS WRONG:
* THE SPEC FORBIDS IT. porting-sdk/schema.json $defs/Webhook declares exactly ten
properties — error_keys, expressions, foreach, headers, input_args_as_params, method,
output, params, require_args, url — under `unevaluatedProperties: {"not": {}}`.
`body` is not among them, so emitting it is a SCHEMA VIOLATION, not a harmless extra.
(Re-read from the schema here, not taken on the brief's word.)
* THE ENGINE NEVER READS IT. mod_openai/actions.c:735-739 and bedrock.c:4920-4926 read
url, method, form_param, `params`, `headers` and nothing else; `grep -n '"body"'`
across both returns ZERO matches.
* THE HELPER SILENTLY DISCARDED THE CALLER'S DATA. `createSimpleApiTool` accepted a
`body` option and forwarded it to `DataMap.body()`, writing a key nothing consumes.
REPRODUCED BEFORE FIXING, not inferred — this is a WIRE fix, not a signature edit:
createSimpleApiTool({..., method: 'POST', body: {query: '${args.q}'}})
BEFORE webhook KEYS: ["body","method","output","url"]
AFTER webhook KEYS: ["method","output","url"]
The caller's payload was riding an invalid key into a void. Both keysets match the
reference's recorded before/after exactly.
TS SHAPE NOTE — this port uses an OPTIONS OBJECT, not python kwargs, so the removal is of
the `body?: Record<string, unknown>` property plus the `if (opts.body) dm.body(opts.body)`
forward. Post-fix the options carry the reference's 7: name, url, responseTemplate,
parameters, method, headers, errorKeys.
WHY port_signatures RECORDED NINE PARAMS. Not an extra beyond `body`: the options-object
emitter explodes the object's 8 declared properties into keyword params and appends a
synthetic `kwargs` var_keyword (the options-object idiom fold). 8 + kwargs = 9. Removing
`body` leaves 7 + kwargs = 8, and the diff tolerates the trailing var_keyword, so DRIFT
lands clean against the oracle's 7.
ALSO CORRECTED — the params() TSDoc. It read "Set query or form parameters", giving no
signal that `body` and `params` are different keys with only one of them read. It now
states the distinction and cites the schema and the engine readers. (ts did NOT carry the
reference's false "alias for body" sentence — the one that propagated into
signalwire-cpp/include/signalwire/datamap/datamap.hpp:90 — so there was nothing to undo.)
SCOPE HELD, matching the reference: `DataMap.body()`, the public BUILDER METHOD, is NOT
removed. It is recorded port surface implemented by all nine ports, and its removal is a
separately-breaking change belonging to its own decision. The three `.body(...)` builder
examples in docs/datamap-guide.md are therefore left in place.
Call sites updated — 4, all of them doc/type, none in production code:
* src/DataMap.ts the option + the forward (the only internal caller)
* docs/api-reference.md the signature block
* docs/datamap-guide.md the signature block, the parameter table row, and the
POST example that passed `body: {q, max}`
No example under examples/ passed `body` to the helper. dist/ is gitignored and untracked
here, so there is no shipped build output to regenerate.
Tests — RED first, and the red landed on the new assertions, not upstream in setup:
* has no body option — passing one is a compile error
type layer: @ts-expect-error on the literal property (tsc rejects it now; before
the fix the directive itself errored TS2578 "Unused")
runtime layer: a cast-through caller still gets no `body` on the wire
* emits no body key and stays inside the schema.json webhook properties
asserts the emitted webhook's keyset is a subset of the schema's ten
Verification:
BEFORE 136 files / 2884 tests passed, 0 failures (measured on the stashed clean tree)
AFTER 136 files / 2886 tests passed, 0 failures — the +2 is exactly the two new tests
drift.sh typescript -> exit 0 (was exit 1 with the single
`param[6] (error_keys): type 'optional<list<string>>' vs 'dict<string,any>'` mismatch
that appeared when porting-sdk 2410c9e regenerated the oracle onto f171ce3)
SURFACE suite -> exit 0, all 7 rules PASS
run-lint.sh / run-format.sh -> clean (formatter a no-op on the change)
run-ci.sh -> `==> CI FAIL (gates: BEHAVIORAL)`, 25 gates PASS, one rule red:
BEHAVIORAL:SWAIG-HTTP-INVOKE `token_absent`. That is PRE-EXISTING and NOT from this
change — d47fbb4 left it deliberately red as a real fail-open finding belonging to the
separate 7c2f253 secure-tool-requires-a-token rollout across seven ports. Confirmed by
re-running the rule on clean HEAD with this change stashed: identical single failure,
`oracle {handler_invoked: false, refused: true}` vs
`typescript {handler_invoked: true, refused: false}`.
SEMVER-DIFF now reports the removal as BREAKING against the last release (3.0.0), required
bump = major, actual = none. The rule is report-only so it still passes, but the entry is
correct and should be settled with the coordinated release bump, not by re-adding the key.
The known ts artifact-checkout hazard did not bite: port_signatures.json still carries the
7-param shape after the full run-ci (suites/surface.py now snapshot-restores pre-run bytes
via TreeGuard instead of `git checkout`, which is what used to revert an unstaged regen).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
SIGNATURES-FRESH and DOC-SURFACE-FRESH both compare a fresh enumeration against `git show HEAD:<artifact>`, so the preceding SWAIG regen staled both the moment it landed. Regenerated with the port's own enumerators, nothing else touched: npx tsx scripts/enumerate-signatures.ts --out port_signatures.json npx tsx scripts/enumerate-doc-surface.ts --output docs_audit_surface.json Both diffs are exactly the previous commit's four field changes and nothing more. port_signatures.json: SwaigAction.SWML and SwaigRequest.SWMLCall/SWMLVars appear; extensive_data and functions_on_speaker_timeout go bool -> union<string,bool>. docs_audit_surface.json: the same three member names, plus its `generated_from` provenance line. The enumerators were run with --out/--output explicitly. Without the flag enumerate-* writes nothing and still exits 0, which leaves a stale oracle looking like a clean no-op diff; the 99-module / 2445-method write line is the receipt that real content landed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MjEro9sSs5TLq66rTzSq6Z
…prose
getVerbDescription is an accessor over whatever description text the schema
happens to carry. Descriptions are editorial, owned by the docs pipeline
(relocated to api-reference-specs on 2026-08-07), not wire facts owned by this
SDK. Two of the three assertions encoded editorial content as a port contract:
- `expect(desc).toContain('End the call')` for hangup pinned the literal
English. A copy edit in another repo -- "Terminates the call" -- reds this
port for a reword that changed no behaviour.
- `expect(desc.length).toBeGreaterThan(0)` for tap REQUIRED tap to be
enriched. If docs declines to enrich a verb (deprecated, internal, not yet
written), the port fails on an editorial decision made elsewhere. Per the
owner: "it shouldn't fail without the enrichment if docs chooses to not
enrich for example a deprecated."
Both are now behavioural and driven from the fixture rather than from prose
spelled out in the test: for every verb the schema enriches, the accessor must
surface exactly that schema text; for every verb it does not, ''. Those hold
whatever docs writes, and they are strictly stronger than what they replace --
`toContain('End the call')` passes on a truncated or padded description, an
exact fixture comparison does not.
Adds the case the original trio never covered: a verb that EXISTS but is
deliberately NOT enriched must return '' and must not throw. The bundled schema
enriches all 39 of its verbs, so this loads a copy with one verb's description
stripped, and checks that dropping the prose degrades nothing else (the verb
still lists, still exposes properties, still validates).
The un-enriched block guards against testing the wrong schema. loadSchema()
falls back to the bundled schema SILENTLY on a load failure while `schemaPath`
still reads back whatever was passed in -- a bogus path yields both the bogus
readback and the full 39-verb bundled schema -- so the guard asserts the loaded
CONTENT, not the path readback, which would have been vacuous.
`toBe('')` for an unknown verb was already correct accessor behaviour and is
kept as-is.
Negative-controlled: three seeded accessor defects (return undefined; return the
neighbouring verb's text; truncate the text) each drive these tests RED (3, 2,
and 1 failures respectively) and pristine restores GREEN; the silent-fallback
guard was itself controlled by pointing it at a missing path.
Suite: 133 files / 2815 tests before, 133 / 2821 after, all passing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjEro9sSs5TLq66rTzSq6Z
…pt the string form GEN-FRESH-SWAIG was stale on this branch. The prior regen here (297c5ef) picked up the dea604b vendoring; porting-sdk has since re-vendored swaig-specs from mod_openai at 58e1e00 ("contexts/steps grammar, named prompt keys") and nothing regenerated this port against it. `generate-swaig-payloads --check` exited 1 before this commit and exits 0 after. WIRE-SURFACE CHANGE, not a reformat, and narrow: six params that were `boolean` now also accept `string`, because the re-vendor gave each a `type: ["boolean","string"]` (or, for the two that already carried a literal, an extra `type: "string"` arm in its `oneOf`). ContextSwitchAction.consolidate boolean -> boolean | string ContextSwitchAction.full_reset boolean -> boolean | string PlaybackBgAction.wait boolean -> boolean | string TransferAction.summarize boolean -> boolean | string SwaigAction.extensive_data boolean -> boolean | string SwaigAction.functions_on_speaker_timeout boolean -> boolean | string SwaigAction.back_to_back_functions + string (keeps 'forever') SwaigAction.wait_for_user + string (keeps 'answer_first') The string arm is real wire behaviour, not laxness. Quoting the x-truthiness the re-vendor attached to back_to_back_functions (swaig-specs/swaig-response.yaml:75): read with cJSON_FSTrue (step_management.c:1009) — the JSON literal `true`, or a string case-insensitively one of `yes`, `on`, `true`, `t`, `enabled`, `active`, `allow`, or any numeric string whose integer value is non-zero. A JSON `false` does NOT trigger it, and neither does a JSON NUMBER: the string arm reads `valuestring`, which is NULL on a number — so `"1"` fires the action and `1` does not. Typing these `boolean` alone made the accepted string form unrepresentable. NOT INCLUDED: src/swml_verbs_generated.ts ========================================== The working tree this came from also carried a regenerated swml_verbs_generated.ts (158 interfaces -> 74). That emit is CORRECT and byte-reproducible, but it is deliberately left out, because committing it would trade one red gate for two: GEN-FRESH-SWML wants it to match porting-sdk/schema.json, now 60 $defs DRIFT/SURFACE want it to match the Python oracle, still 169-$defs-shaped The Python reference has not been regenerated against the canonical schema — signalwire-python's swml_verbs_generated.py still declares 159 classes incl. AIObject, and python_surface.json records 158 in that module. Measured on this exact base: committing the regen takes SURFACE from exit 0 (all 7 rules PASS) to exit 1 with DRIFT + SURFACE-FRESH red — SURFACE-FRESH reporting 118 differing leaves, all of the form `classes.AIObject[0]: committed='SWAIG' fresh='<absent>'`. That is an upstream fan-out (python first, then the ports), not a TS-local fix, and it is owner-held. GEN-FRESH-SWML stays red here meanwhile. The 158 -> 74 deficit is a property of the SCHEMA, not of this generator — verified by running the generator unmodified against the legacy 169-$defs schema.json (porting-sdk 756ea55): it emits 158 interfaces + 34 types, and its symbol set is identical to the committed file's, 192/192. Verified: vitest 139 files / 2932 tests passed, tsc --noEmit exit 0, run-lint.sh exit 0, run-format.sh a no-op on the generated output, generate-swaig-payloads --check exit 0, and the SURFACE parity suite fully green (exit 0: SIGNATURES/DRIFT/SURFACE-FRESH/SURFACE-DIFF/GEN-TYPE-DEGENERACY/ GEN-IDIOM/SEMVER-DIFF all PASS) — this change is surface-neutral. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PbNcuqH8o9WEoEnT24q4wB
…ktrees
`npm test` failed one suite that passes 7/7 in isolation. Two independent
defects compounded; both are fixed here.
1. tests/SecurityConfig.configFileTls.test.ts built its fixture dir as
join(process.cwd(), '__config_file_tls_test_tmp__'). cwd is a property of
the RUNNER, not of the file, so every copy of this test resolved to the
SAME directory. It is now derived from the test file's own location and
made unique per run with mkdtemp into the gitignored .sw-tmp/ -- the
convention already used by SchemaUtils.verb.test.ts. Two concurrent copies
can no longer collide by construction.
2. vitest.config.ts declared no `include`, so vitest's default glob swept the
whole tree -- including sibling agent worktrees under .claude/worktrees/*.
Measured: 955 files collected, 816 of them (85%) belonging to OTHER
checkouts, among them four copies of this suite, all EXECUTED. Four copies
then raced on the one fixture path until one copy's afterAll rm -rf deleted
it mid-openssl.
Chose include over exclude deliberately. An exclude list is open by
construction: it bars only the directory shapes we thought to name, so the
next stray checkout starts running again and the resulting failure looks like
a flake, because whether the suite is green depends on what else happens to be
on disk. An include is closed: every test this repo runs lives under tests/.
Neither fix serialises anything -- fileParallelism stays on. Isolation comes
from scoping the path each run owns.
Verification:
- before, 4 copies in one run: Test Files 3 failed | 1 passed (4)
Tests 14 failed | 14 passed (28)
- after, same sibling worktrees present: Test Files 1 passed (1)
Tests 7 passed (7)
- fix (1) controlled independently by forcing all 4 on-disk copies into one
run via a temporary config: the fixed copy never appears in a FAIL line,
and all 6 fixture-path errors cite the old shared __config_file_tls_test_tmp__,
never the new per-run swts_config_file_tls_* path.
- collected files 955 -> 139: 0 added, 816 dropped, and 0 of the 816 lie
outside .claude/worktrees/. No legitimate test stopped running.
- full suite: Test Files 139 passed (139), Tests 2932 passed (2932)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbNcuqH8o9WEoEnT24q4wB
… engaged
verbTopLevelPropertyNames tested `body['type'] !== 'object'` on a verb's config
node and returned null otherwise. A union node — `{ anyOf: [...] }` — carries no
`type` of its own, so that test failed and the resolver bailed. The caller reads
null as "no key-set to enforce" and answers valid. The check therefore did not
report a problem; it stopped checking and reported success, which is strictly
worse than failing.
This was live against the shipped schema.json, not contingent on any future
re-vendor. Five verbs are union-shaped there:
connect oneOf of 4 $refs (ConnectDeviceSingle/Serial/Parallel/SerialParallel)
play oneOf of 2 $refs (PlayWithURL / PlayWithURLS)
send_sms anyOf of 2 $refs (SMSWithBody / SMSWithMedia)
sleep anyOf of object-with-duration / integer / SWMLVar
unset anyOf of string / array-of-string
Four of the five have object branches with perfectly enumerable keys, and the
resolver was returning null for all four.
The resolver is now a shared recursive closedKeySet handling three node shapes:
a $ref (followed into $defs), a union (resolved branch by branch and UNIONED),
and a plain closed object. The union semantic is the correct one for this
schema shape: a config satisfying an anyOf/oneOf satisfies SOME branch, so a
key belonging to no branch belongs to no valid document. Non-object branches
(sleep's bare integer, SWMLVar) contribute no keys — they constrain the config
to not be an object at all, a different question from which keys an object
config may carry. A depth bound keeps a self-referential $ref from spinning.
Shapes with genuinely no closed key-set stay disengaged, and are pinned by test
so this is not read as "always enforce something": `set` is an OPEN object
(unevaluatedProperties:{} with no `not`, zero declared properties — a free-form
variable bag by design), `unset` is a union with no object branch, and
cond/label/return are array/string/untyped.
Engaged verbs go 30 -> 34.
SEPARATE DEFECT FOUND, NOT FIXED HERE — connect is unvalidated on the Ajv path.
Ajv cannot compile the connect verb at all: connect's `result` reaches
$defs/SWMLAction, whose `SWML` property is `{"$ref": "SWMLObject.json"}`, an
external file absent from the bundled schema's $defs. Ajv resolves refs eagerly
and throws "can't resolve reference SWMLObject.json from id #";
getVerbValidator swallows that in its catch and returns null, and validateVerb
falls back to validateVerbLightweight — which finds no top-level `required` on a
union node and passes ANY config. So `connect` accepts arbitrary keys end to end
for a reason unrelated to the union resolution fixed here. Fixing it means
resolving or vendoring SWMLObject.json (a schema-artifact change) or changing
Ajv's ref-resolution policy, neither of which belongs in this commit. Go does
NOT share the symptom: it compiles the whole document with santhosh-tekuri,
which tolerates the unresolved ref, and rejects the same forbidden key. The
test file documents this in place of asserting a behaviour that is broken for
an unrelated reason.
Negative control, both directions, on this branch:
pre-fix 12 of 21 tests FAIL (all four union verbs, all three directions)
post-fix 21/21 PASS, including the legitimate-config direction that would
catch an intersection computed in place of a union.
The same defect is present in the go and dotnet ports, which hand-maintain an
equivalent resolver with the identical bail.
…tion
`validateVerb('connect', {to, zzz_not_a_real_key})` returned
`{"valid":true,"errors":[]}` — a config nobody validated came back clean.
The bundled schema.json holds exactly one non-local `$ref`:
`$defs/SWMLAction.SWML -> "SWMLObject.json"`, a sibling spec file that is not
bundled. Ajv resolves refs EAGERLY, so compiling any verb whose `$defs` subtree
reaches `SWMLAction` threw `can't resolve reference SWMLObject.json from id #`.
`getVerbValidator`'s bare `catch` swallowed that and returned null, and
`validateVerb` then fell through to `validateVerbLightweight`, which only checks
required props — so a COMPILE FAILURE read as a PASS.
The blast radius was never just `connect`: 8 of 39 verbs degraded this way, all
via `... -> Action -> SWMLAction -> SWMLObject.json` —
ai, ai_sidecar, amazon_bedrock, cond, connect, execute, join_conference, switch.
Two independent fixes:
1. Ref policy (the root). Register a permissive placeholder for the schema's
unbundled external ref on our own Ajv instance, so the ref RESOLVES and
compilation succeeds. Every other constraint — crucially the
`unevaluatedProperties` closure that rejects unknown keys — is then enforced
normally; only the nested `SWML` payload goes unchecked. This is exactly how
go's santhosh-tekuri validator already behaves, which is why go never showed
the symptom. schema.json is NOT modified, vendored, or reinterpreted.
2. Loud degradation (the backstop). A compile failure is now recorded and
reported as an error instead of falling through to the permissive lightweight
check, so validation that did not happen can never again read as `valid:true`.
New `compileFailedVerbs` / `precompileVerbValidators()` make the condition
observable rather than silent.
Both directions covered by 9 regression tests: a forbidden key is rejected and a
legitimate config still passes on each of the 8 verbs, and the guard is proven by
reinstating the pre-fix Ajv instance and asserting a refusal rather than a pass.
Negative-controlled — with the ref policy disabled, 9 of them fail.
Full suite: 133 files / 2835 tests passed. SURFACE's DRIFT/SURFACE-FRESH/
SURFACE-DIFF failures reproduce identically on clean main and are unrelated.
Supplying the real SWMLObject.json remains an owner-held schema-artifact change
(held under #223); afterwards the placeholder in UNBUNDLED_EXTERNAL_REFS can be
dropped and the nested SWML payload becomes fully validated too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbNcuqH8o9WEoEnT24q4wB
GEN-FRESH-SWML could exit 0 having compared nothing. generate-swml-verbs.ts
guarded both porting-sdk spec reads with fs.existsSync and fell through to a
friendly "skipped ... using committed <file>" log. Under --check that is a false
green: the skip means emitFile is never called for that output, so nothing lands
in staleFiles, so finalizeCheck's `if (CHECK && staleFiles.length)` is false and
the process exits 0. The committed file is never compared against anything.
Measured on this repo with PORTING_SDK pointed at a copy whose only difference is
a removed schema.json:
before rc=0
checked src/SwmlVerbMethods.generated.ts (39 verb methods)
checked src/PlatformContracts.generated.ts (9 types)
skipped SWML verb contracts (no schema.json at .../schema.json; using
committed src/swml_verbs_generated.ts).
All 192 committed SWML verb config types went uncompared while the gate reported
success. This matters now: porting-sdk/schema.json is the legacy hand-maintained
SWML spec slated for deletion (porting-sdk task #199). On the day it is removed,
the other nine ports' generators die loudly and this one would have gone green.
Both spec sources are TRACKED files in porting-sdk, so once psdk itself resolves
they are present in every legitimate configuration — there is no caller that
genuinely needs the skip. The one real soft-fail case (a published consumer with
no adjacent porting-sdk) is the separate `!psdk` branch above, which already
treats an unverifiable --check as a hard failure (exit 2). These two guards were
the inconsistent ones; they now match that precedent. No opt-out flag is added,
because no legitimate caller needs one.
Non-check generate runs fail hard too: emitting a partial tree while exiting 0
would silently leave a committed file at a stale revision.
after rc=1
generate-swml-verbs: SWML verb contracts: spec source not found at
.../schema.json. Refusing to skip — under --check a skip would leave
the committed src/swml_verbs_generated.ts compared against nothing and
still exit 0 (a false green).
The happy path is unchanged (rc=0, all three files checked: 39 verb methods /
9 platform types / 192 verb config types) and the committed generated files are
NOT stale — no generated output is modified by this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjEro9sSs5TLq66rTzSq6Z
… not drift
Measurement-only lane. No generated file, generator, or oracle is modified.
The row recorded signalwire-typescript as "GEN-FRESH stale: 192 exported symbols
vs the generator's 78" and invited four readings: port drift, generator
regression, a units error, or a stale pin. Re-deriving both numbers:
COMMITTED exports: 192 {'interface': 158, 'type': 34}
FRESH exports: 78 {'interface': 74, 'type': 4}
Both are src/swml_verbs_generated.ts, one file, measured by one regex — the
generator's own "(78 types)" stdout is decls.length, and swmlDeclaration() emits
exactly one top-level export per decl, so the fresh file measures 78 both ways.
Like-for-like: YES. The numbers survive; the LABEL on them did not. They were
never "port surface vs generator surface".
Root cause is upstream and mechanical. porting-sdk/schema.json went 169 $defs ->
63 at psdk fb6defc (2026-08-07, "drop the hand-edit and re-vendor swml; the
extractor derives 11"), then -> 60. This port's file was last written 2026-07-26
by d30dcd4, from the 169-def era. The generator input path never moved
(generate-swml-verbs.ts:637 has read psdk/schema.json since f41d446).
Three hypotheses tested and falsified, each by running the generator:
- today's re-vendor 203eb90 — regen against 203eb90^:schema.json also yields 78
- HEAD 0c7f923 — its generator AND its parent's both yield 78; that commit
REVEALED the staleness by making an fs.existsSync skip fail loudly instead of
exiting 0 having compared nothing
- port drift — zero references to the module anywhere outside src/ and two
oracle JSONs, so no hand-written code could have grown it
Also corrected: the rule literally named GEN-FRESH is GREEN. Two other rules in
the five-rule family are red — GEN-FRESH-SWML (this) and GEN-FRESH-SWAIG (two
added optional fields, purely additive).
INVENTED count: 0. All 139 lost symbols are STALE-GENERATED — 126 are $defs keys
in the 169-def schema the port still vendors at src/schema.json, and the other 13
are <Verb>Config names the generator synthesizes at generate-swml-verbs.ts:188.
Classifying on $defs membership alone would have produced 13 false INVENTED
findings, the most serious class.
Blast radius of a regen, if it is ever run: it deletes 139 exported type names
and adds 25. Not a package-API break — nothing under src/ imports the module and
src/index.ts does not export it — but 59 of the 139 are cited as declared types
in port_signatures.json, so the surface oracle moves. Export-reachability and
gate-reachability are independent questions here.
Not a stale pin: no pin exists. nightly-multi-os.yml:102 floats to psdk main.
Fleet: go carries the identical 192 decls from the identical 2026-07-26 date, so
this is a ten-port spec-fanout event, not a typescript defect — exactly what
porting-sdk/scripts/spec_fanout_gate.py:9 predicts.
Left unapplied deliberately. Whether fb6defc's shrink is INTENDED to remove 139
SDK type names, or the new extractor under-derives, is an upstream spec question;
answering it in one port answers it for all ten. No allow-list entry is
appropriate — there is no divergence, only a not-yet-run regen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXK6fU1ASoT14yniFHhWRh
…-fanout staleness, not drift
…op the widen relaxation
The bundled schema listed three hangup reasons (hangup|busy|decline) and marked
the node `x-sdk-widen: true`; SchemaUtils read that marker and stripped the
value constraint before compiling the Ajv validator. So every string validated,
including ones the server refuses.
The engine's contract is stated once, in C, at
mod_infrastructure/relay_apis.c:1105:
JSON_CHECK_STRING_MATCHES_OPTIONAL(reason, "hangup,cancel,busy,noAnswer,decline,error")
and a non-match is a hard reject (libks ks_json_check.h sets *error_msg and
returns 0). The SWML layer types the field as a bare string
(swml_schema.c:1571) and swml.c forwards it verbatim into the `end` RPC on the
same call, so the contract a document must actually satisfy is the COMPOSITION
of the two layers — exactly these six values.
Two ordered changes, because either alone is a regression:
1. src/schema.json's hangup.reason node now publishes the six engine values as
a real enum and no longer carries x-sdk-widen. Three of the six (cancel,
noAnswer, error) were absent from the old three-const union, so they
validated ONLY because widen removed the constraint — deleting the reader
without this would have turned three engine-valid reasons into client-side
rejections. The edit touches only that node.
2. applyWiden and its call site in SchemaUtils are deleted. The
`x-sdk-widen` suite in tests/SchemaUtils.verb.test.ts asserted that
'user_hangup', 'no_answer' and 'anything-at-all' must validate, and the
SwmlBuilder block asserted an arbitrary off-enum reason is accepted;
relay_apis.c:1105 refuses all of them, so those rows pinned a bug. Both are
replaced with suites that pin the engine contract in both directions. Note
the engine spells it camelCase "noAnswer"; "no_answer" is not an engine
value in any spelling.
The behaviour change is INTENDED and STRICTER: a caller who passed "no_answer"
previously got an opaque server-side rejection at call time and now gets a
clear client-side validation error. A guard on the artifact is included so a
re-vendor that reintroduces the three-value union or the marker is caught.
scripts/_gen-common.ts's x-sdk-widen branch is untouched: it reads the spec
YAML (where the marker remains as an upstream audit record) and affects the
emitted TS type, not accepted values. The generated
`swml_verbs_generated.ts` already declares the six-value union, so the type
layer and the validator now agree.
…protocol/ Ninth and last of the nine relay-protocol generators to move off the legacy `porting-sdk/relay-protocol/` tree (130 standalone JSON-Schema files) and onto the single combined document `porting-sdk/combined-specs/relay.yaml`. APPROACH: the reader is PORTED to TypeScript (`scripts/_relay-shapes.ts`), the way Go ported it, rather than shelling out to the Python reader the way the six interpreted ports do. This generator is one of five siblings that all read their spec in-process with `js-yaml` — already a direct dependency of `_gen-common.ts`. Spawning `python3` would add a cross-language runtime dependency (interpreter + PyYAML) that no other TS generator carries, and that `resolvePortingSdk()`'s deliberately fail-soft contract could not degrade around. The Python module is the spec this file implements; the two are kept honest by GEN-FRESH-RELAY byte-comparing the emitted output. Verified equivalent: both readers return the same 128 nodes, in the same order, byte-identical. OUTPUT IS NOT BYTE-IDENTICAL, in exactly three understood ways. All 128 declarations are present, name sets identical, and ZERO have any body or type difference: 1. The header line names the new source (intended). 2. Two declarations REORDER: CallingPlayResult/CallingPlayPauseResult and CallingRecordResult/CallingRecordPauseResult. The legacy generator sorted FILENAMES, where `calling.play.pause.result.json` precedes `calling.play.result.json` because '.' (0x2E) < 'r'. The adapter sorts METHOD NAMES, where `calling.play` precedes `calling.play.pause`. The old order was an artifact of the filename encoding; the new one is the real method order. 3. Six declarations lose their TSDoc summary. The legacy per-file tree carried a `description` on each schema; the combined document does not, because that was a per-file ENVELOPE key that existed only so a standalone schema could re-declare its own identity. But that prose was never hand-written — the extractor GENERATED it from a template over method, phase and the source class name, and `x-source-file` still carries the class name. So the summary is DERIVED from the surviving metadata rather than lost or frozen into a port-local prose table: 122 of 128 reproduce their legacy description EXACTLY, zero mismatches. The remaining 6 (calling.call, calling.conference and messaging.send, both phases each) carried genuinely hand-written prose that the combined document does not carry in any form. Those emit with no TSDoc rather than having a port invent spec text; the document does carry different prose for 4 of them under `unknown_note`, which is a spec-side question, not a port-side one. The camel-case fix at psdk 2e5be42 does unblock ts as briefed — 128 declarations, 0 empty bodies, 124 index signatures, `tsc` clean. But the brief's claim that the missing-`type`/`description` diagnosis was "false" does not survive: those keys are absent from ALL 128 nodes the generator consumes. The 2419/11/1 counts were over the whole document, not these nodes. Camel was necessary, not sufficient. `relay-protocol/` is left in place — other consumers may still read it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MjEro9sSs5TLq66rTzSq6Z
…rried in Local FMT autocorrect from run-ci; committed so the CI --check agrees. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4oecKxqh9QZoLSvdUQwZ7
…orBody
RestError.body (and HttpClient's parse of a non-2xx body) was typed
`string | SignalWireErrorBody` -- a SWML-webhook contract type unrelated to
REST errors -- via an unchecked `JSON.parse(text) as SignalWireErrorBody`.
Type it as what the code actually holds: the raw text, or the parsed JSON
value, naming the error shapes the REST specs declare --
Types_StatusCodes_StatusCode422 ({errors: RestApiErrorItem[]}), relay-rest's
Types_StatusCodes_ValidationError ({errors: SpaceApiErrorItem[]}), and
{error: '<reason phrase>'} (400/401/403/404/500) -- plus any other JSON value,
which is kept as-is like the python reference (body: Any). Every JSON value is
a member of the type, so the parse cast is now sound. No runtime change, no
new public symbol.
Tests use the real wire shapes (both envelopes + an off-spec JSON body the
mock server emits) instead of casting {errors: ['invalid field']}.
port_signatures.json: only the 3 SignalWireRestError body entries change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
… contract; drop SignalWireErrorBody Coordinated with porting-sdk a8b7eaa, where rest-apis/swml-webhooks/openapi.yaml became a rendering of mod_infrastructure's engine-derived `webhook_request` contract. - src/PlatformContracts.generated.ts regenerated (scripts/generate-swml-verbs.ts): closed SwmlRequestData (vars required), SwmlRequestCall = union of the Phone/Sip/Webrtc/Other variants (call_id/node_id/call_state/direction required, closed enums), Parent/Peer, header items, SipSipData. The superseded SwaigRequestData/PostPrompt* and SignalWireErrorBody leave the generated file with the spec. - SignalWireErrorBody re-exports removed (src/PlatformContracts.ts, src/index.ts); no users since 9b9ba52 (owner-approved removal). - untyped-JSON boundaries (AgentBase, SWMLService, InfoGathererAgent) that hand a parsed body to SwmlRequestData-typed hooks now say so with an explicit cast: the type is static-only and the body is not validated; runtime behaviour is unchanged. - port_surface.json / docs_audit_surface.json: only the swml_webhooks_types entries patched, from a fresh enumeration against porting-sdk a8b7eaa. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
…ix jargon + example to the call union Follow-up to f388483, whose closed/required emission broke LINT (examples + tests typecheck) and PUBLIC-JARGON on TS Test run 36405130242: - scripts/generate-swml-verbs.ts: the platform contracts are a READ-SIDE payload, so they are declared with swmlDeclaration (all fields optional + `[key: string]: unknown`), the same convention as the SWML verb read-side types and Python's total=False TypedDicts. Field TYPES stay engine-derived (closed enums, SwmlRequestCall = the per-device-type union). Existing handler signatures and partial bodies type-check unchanged, so the explicit casts f388483 added in AgentBase / SWMLService / InfoGathererAgent are reverted (those files are back to 9b9ba52). - examples/advanced-dynamic-config.ts: `call.from` is read through the union (the `other` variant carries no `from`). - src/PlatformContracts.ts: doc comment no longer names an internal repo (PUBLIC-JARGON). port_surface.json / docs_audit_surface.json entries re-verified against a fresh enumeration (unchanged from f388483). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKNEp6YffXdC6RyDjtU4tk
…po variable
The coordinated-pass pin was the repo variable PORTING_SDK_REF, which every
branch's CI reads -- main and everyone else's PRs were checked out against our
in-progress porting-sdk branch. The variable has been deleted; the pin now lives
in a file committed only on the coordinated branch:
- .porting-sdk-ref (this branch: wave6/ctor-dunder-fold). No file -> main, so
main and every other branch are unaffected. DELETE it in the PR that merges
the coordinated set (porting-sdk/COORDINATED_PASS.md).
- .github/coordinated-ref.sh: byte-identical copy of porting-sdk's canonical
resolver. A `coord` step after the repo's own checkout validates the pin
(branch-name characters only) and emits ref / ref_<name> outputs.
- every porting-sdk / signalwire-python checkout takes
${{ steps.coord.outputs.ref }} / ref_python instead of the variable; publish
and release workflows stay hard-pinned to main.
- run-ci.sh: COORDINATED-REFS description + comments name the pin file.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BtqaxuBHY5jMZUQqk7mP5F
The branch-local pin (.porting-sdk-ref, optional .signalwire-<name>-ref) moves from the repo root into .github/ so signalwire-python's ROOT-HYGIENE stays strict with no allowlist entry (owner, 2026-09-29). Re-copies the canonical .github/coordinated-ref.sh, which now reads .github/porting-sdk-ref and fails on a pin at the retired root spelling. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BtqaxuBHY5jMZUQqk7mP5F
Brings PR #179 current with main: 253 commits, the shipped 3.5.0 baseline (Python 3.5.1 sync, security fix pass, AI Chat gateway/handoff router, swaig-test DataMap simulator, sw-tsdocs, tutorials). 40 files conflicted. Resolutions, by area: - package.json / package-lock.json / CHANGELOG.md: main's. 3.5.0 is published (tag v3.5.0 on main), so the branch's 3.0.0 consolidation of the 3.0.2/3.1.0/ 3.2.0 entries no longer describes history; the branch changed nothing else in these files. - DataMap.body() + createSimpleApiTool({ body }): main's. The pinned oracle (porting-sdk wave6/ctor-dunder-fold 2fc3bf2) has DataMap.body and create_simple_api_tool(body=) again; main's body() writes `params`, the key the platform reads. Branch's removal tests and docs dropped with it. - addLanguage / LanguageConfig: combined. Branch: voice REQUIRED (schema `LanguagesWithFillers.required`, reference required positional) and the "engine.voice:model" split. Main: flat speechFillers/functionFillers with the older keyed forms and `fillers` still accepted (flattened, warned). Emission order keeps the branch's (fillers after speech_model). The tutorial's AgentConfig.languages now types voice as required to match. - AgentBase: secure-tool token check takes main's swaigTokenRefusal() (same absent-token refusal and reference wording as the branch); contexts take main's raw-dict path (ai.prompt.contexts, both sides agree); onSummary keeps the branch's optional rawData with main's widened return type; getBasicAuthCredentials keeps the `= false` default and adds main's 'config file' source. debug_webhook_url/_level stay inside `params` (branch, matches agent_base.py); docs updated to say so. - defineContexts: a supplied dict is still stored in the prompt manager; a supplied ContextBuilder is no longer snapshotted there, since snapshotting an empty builder throws and main's callers fill the builder after passing it. - SWMLService config file: the branch's snake_case `security` section reader, with main's semantics folded in (empty values set nothing, basicAuthSource = 'config file', legacy `security.basicAuth` still read). - SchemaUtils: main's document-schema compile for SWML-embedding verbs (real validation) replaces the branch's permissive SWMLObject.json placeholder; the branch's loud compile-failure record is kept and shared through main's COMPILED cache. - ServerlessAdapter, swaig-test, BedrockAgent: main's (branch changes there were doc-only, or superseded: main's fromEvent reads rawQueryString). - WebService.start: main's credentials refusal + the branch's `host = '0.0.0.0'` parameter default. - InfoGathererAgent: main's request-derived query/headers with the branch's optional rawData. Prefab/skill/ai-chat/livewire doc conflicts: main's facts, without the comparisons to other SDKs the branch removed. - scripts/enumerate-signatures.ts: both module mappings; the branch's srcRelStem() (main's equivalent srcSubpath() dropped). - PORT_OMISSIONS.md / PORT_SIGNATURE_OMISSIONS.md: the branch's deletions plus the entries main added for symbols main introduced (no new entries authored). - Tests: both sides' coverage kept; three branch tests adjusted for main's behaviour (WebService needs credentials; mcp_gateway's TLS flag is _insecureTls and setup() runs an SSRF check first), one main test adjusted for debug events riding in params (no longer a Bedrock left-out feature). Regenerated against porting-sdk 2fc3bf2 (the pinned wave6/ctor-dunder-fold): port_signatures.json, port_surface.json, docs_audit_surface.json. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…at 434/435 main's src/cli/simulation.ts exports ServerlessSimulator without a TSDoc block; main's DOC-SURFACE floor is 77.8%, this branch's is 100.0%, so the merge combination dropped coverage to 434/435 and DOC-SURFACE went red (run 36813922191). Locally: 100.0% (435/435). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…held until wave6 lands Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…pec markup
Generators (scripts/):
- _gen-common: an empty CLOSED object (`properties: {}` + additionalProperties:
false, verto.attach/verto.pong results) emitted `export interface X
Record<string, unknown>`, a syntax error that broke the package build. A
memberless top-level object is now a type alias (`Record<string, never>` when
closed).
- generate-rest-types reads rest-apis/<ns>/openapi.enriched.yaml (owner ruling
2026-09-28, the reference generator's input); discovery stays on openapi.yaml.
- x-sdk-autofill: uuid4 (control_id is generated when omitted) and
x-sdk-compat-kwargs (calling.record `audio` -> params.record.audio), mirroring
generate_python_rest_types.py.
- Embedded path params (`{id}.mp3`), redirect-answer GETs (recordings/room
recordings download, billing_statement.pdf -> the Location URL, never
followed), text successes (billing_statement.csv), and `in: header` params
(space top-up Idempotency-Key).
- PAT-authenticated specs (rest-apis/space) are wired to a separate
Personal-Access-Token HttpClient (`_wireResources(http, patHttp)`).
- POSITIONAL_COMPAT: the spec relaxing/reordering required fields would have
moved published positionals (dial(from, to), createGuestToken, Messages.update,
ShortCodes.update, VideoStreams.update, aiStop, tap order, createOrder's
options) -- pinned to the v3.5.0 shapes so published calls keep working.
SDK: RestClient `personalAccessToken` (SIGNALWIRE_PERSONAL_ACCESS_TOKEN) for
client.space, a refusing stand-in for a missing credential; HttpClient
getText / getRedirectLocation / per-call headers. relay Call.record audio type
follows params.record.audio.
Tests/examples/docs: createInviteToken (never worked; hidden) removed; tap /
play / refer use the shapes the gateway accepts (relay_apis.c JSON_CHECK);
fabric call_flows/conference_rooms plural paths; tests for autofill, compat
kwarg, published positionals, redirect download, and PAT auth.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…the regen Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
- src/schema.json re-bundled from porting-sdk (port_schema_bundle.py sync) with its schema.json.sha256 record; new SCHEMA-BUNDLE gate in run-ci (cheap tier). - generate-swml-verbs: deprecated verbs (dial/eval/if) are dropped and inline objects are hoisted to named types exactly as the reference generator names them (AiConfig, AiSWAIGInternalFillers, ...); the SWAIG envelope types are imported from SwaigActions.generated.ts instead of declared twice. Verb methods take the named config type plus the scalar/positional forms the body accepts, always optional (never narrower than before). - _relay-shapes: serve only the switchblade-carried shapes (extracted_by stamp), as relay_protocol_shapes.py does; the 9 engine verto.* results are not surface. - SchemaUtils: deprecated verbs stay known to validation but are not installed as methods; getVerbProperties returns the verb's object form when its body is a union of forms; the closed-key resolver follows the #223 contract (exactly one closed object arm, else disengage -- identical to union on this artifact, 47/53 verbs engaged); Ajv gets an equivalent additionalProperties form where it mis-tracks unevaluatedProperties beside a multi-entry dependentSchemas (cond). - AgentBase: the ai verb's `multilingual` validation bypass is removed (the schema declares it). - FunctionResult.changeVoice (reference change_voice); SchemaUtils' unpublished test hooks renamed _compileFailedVerbs / _precompileVerbValidators. - Regenerated port_signatures / port_surface / docs_audit_surface. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
Generators:
- _gen-common: a tuple-form array (prefixItems, no items) is the union of its
prefix item types; the empty schema `{}` is `unknown` (py_type `Any`); in the SWML
generator an allOf of scalar constraints is their intersection
(_scalar_allof_intersection), so a verb's bare-scalar shorthand types as the
scalar rather than `string & (...)`.
- generate-rest-types: an array response returns `T[]` (list_voices); a pinned
method's non-pinned REQUIRED field is a required options member (createOrder's
phone_numbers -- `createOrder(id, { phone_numbers })` keeps working); the `_fields`
literal lists positional fields in the reference's keyword order; header args are
assembled first; redirect GETs carry no per-call headers (as the reference).
Enumerator:
- Record<K, V> in generated payload interfaces keeps its value type
(`dict<string,class:Context>`); the written-node path reads Record, unknown and
parenthesized members; quoted wire keys (`nomatch-output`) are fields.
- A command param literally named `params` (ai_sidecar) / a body field named `body`
(Messages.update) are keywords, not the query tail / single-body param; header
args are keywords; positional exploded fields are recorded in reference order; a
request_options already followed by a real param is not hoisted past it; a rest
parameter is not required.
SDK: HttpClient get/post/getText take `options?: { headers }` (the reference's
keyword-only headers); stripControlChars also accepts the processor-call form
(last argument is the record, as the reference); filterSensitiveHeaders is generic
over the header value type.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…Aliases never are) The python reference emits every non-object schema as a module-level TypeAlias -- 456 of them across its generated type modules -- and griffe records none of them (0 in python_surface.json). The TS enumerator surfaced the port's matching `type X =` aliases, which only manufactured port-side additions; it now skips them in generated type files, and the 82 PORT_ADDITIONS entries that existed only to excuse those aliases are deleted (SURFACE-DIFF reported each as DEAD). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…te removed - rest/docs/client-reference.md + the RestClient TSDoc document the Personal Access Token credential (SIGNALWIRE_PERSONAL_ACCESS_TOKEN) and client.space (DOC-ENV). - docs/DISC-g-tsfresh.md deleted: an internal diagnosis note (machine paths, python fences, phantom names) about the stale SWML artifact this regen replaces (ROOT-HYGIENE / DOC-AUDIT / DOC-LANG-PURITY / ACCESSOR-TRUTH). - Public doc comments carry no internal tracking references (PUBLIC-JARGON); the stripControlChars implementation signature is documented (DOC-SURFACE). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
… doc snippets compile
The empty schema `{}` -> `unknown` rendering (the reference's `Any`) is now scoped to
the SWML generator, like the scalar allOf intersection; applied globally it narrowed
published types such as SwmlRequestData.params/envs from Record<string, unknown>.
Doc snippets updated to the spec's shapes (no brand_id on campaign create -- the brand
is the path; cXML applications take call_request_url) and the PAT example imports
RestClient (SNIPPET-COMPILE).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…PET-COMPILE) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…ck ignores an unlisted voice_id The mock's strict wire check rejects brand_id on relay-rest.create_campaign, and the engine-derived schema marks an unlisted Bedrock voice_id as ignored, not rejected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ1TTfYuK2bGybf2Cf9KQv
…ce555f6) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JK98iDuVZQ6gQBo3Zj3RAb
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.
Retires the
PORT_SIGNATURE_OMISSIONS.mdentries the shared-diff ctor/dunder foldmakes dead, per
ALLOWLIST_DISCIPLINE.md:495:This is a deletion-only pass. No source change, no new omission/addition/allow-list
entry, and
PORT_OMISSIONS.md/PORT_ADDITIONS.mdare untouched (different tool, harddead-entry gate).
Counts
Measured with the gate's own
parse_omissionsagainstporting-sdk/python_signatures.json:PORT_SIGNATURE_OMISSIONS.mdentries__init__entries__init__dunder entries249is the count onorigin/main. (The Wave-6 dispatch table cited 247 — that wasmeasured on the sibling
wave6/ledger-burndownbranch, which had already removed 2loggerentries. The 41-entry ctor target is identical either way.)The excused-divergence count is UNCHANGED at 642 across a 41-entry deletion. That is
the direct proof the entries were dead paperwork rather than load-bearing: the fold skips
those symbols before the ledger is consulted, so had any entry been excusing a real
divergence, deleting it would have surfaced one.
The construction node is unchanged
__init__-as-a-member and the §10constructionnode are different contracts, andonly the former folds.
port_signatures.json:All 41 classes remain compared by name in
compare_construction. That is thecomparison that is actually meaningful for a TS constructor — the member comparison
matches params by POSITION, which says nothing useful when a Python wide-kwargs
__init__is held against a TS options-object constructor.
__init__entries the rule does NOT cover — 18 RETAINEDThese classes have no
constructionentry, so the member comparison is the onlycomparison there is. Folding them would trade a visible ledger entry for a real blind
spot, which the rules forbid — they stay:
signalwire.core.pom_builder.PomBuilder.__init__signalwire.rest._base.CrudResource.__init__signalwire.rest._base.CrudWithAddresses.__init__signalwire.rest._base.ReadResource.__init__signalwire.livewire.*(14):Agent,AgentServer,AgentSession,CartesiaTTS,DeepgramSTT,ElevenLabsTTS,LLM,OpenAILLM,RunContext,STT,SileroVAD,StopResponse,TTS,ToolErrorGate output actually seen
DRIFT — the real gate path (
drift.sh, which supplies--numeric-monotypefrom thisrepo's committed
.drift-numeric-monotypemarker; TS is a single-numeric-type language,and without that flag the invocation reports ~20 spurious
intvsfloatfindings):Identical result with a fresh re-enumeration (
drift.sh . npx tsx scripts/enumerate-signatures.ts) — exit 0, 642 excused.Full CI:
Exit 0. All 22
tier=prgates PASS.Two findings, both pre-existing and out of scope here
Stale rationale on a deleted entry. The removed
signalwire.agents.bedrock.BedrockAgent.__init__entry read "reference signaturesoracle records no BedrockAgent class". That is no longer true — the oracle's
construction node records
BedrockAgentwith 7 params (max_tokens,name,route,system_prompt,temperature,top_p,voice_id). The ctor IS compared, so theentry was doubly dead. (The sibling
BedrockAgent.set_prompt_llm_params/set_post_prompt_llm_paramsentries carry the same stale claim and are still present —worth a look, but they are method entries and outside this fold.)
port_signatures.jsonis stale onmain. A freshnpx tsx scripts/enumerate-signatures.tson cleanorigin/mainproduces an 11-linediff: the
Deviceunion gainedclass:signalwire.relay.types.FabricDevice. Introducedby 1a15395 (merged as Add typed fabric device to Relay connect (#19964) #166,
rpc/apex-19964-fabric-connect-device), which changedsrc/relay/types.tswithout re-committing the artifact. It does not turn CI red —SIGNATURES regenerates the file and DRIFT consumes the fresh copy, while SURFACE-FRESH
byte-compares only
port_surface.json— so the drift is invisible to the gates.Deliberately NOT included in this PR, which is a ledger-only diff; it wants its own
regen commit.
🤖 Generated with Claude Code
https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
Coordinated-With: porting-sdk@wave6/ctor-dunder-fold