wave6: retire dead ctor entries (ALLOWLIST_DISCIPLINE §495, shared-diff fold) - #73
Open
mjerris wants to merge 121 commits into
Open
wave6: retire dead ctor entries (ALLOWLIST_DISCIPLINE §495, shared-diff fold)#73mjerris wants to merge 121 commits into
mjerris wants to merge 121 commits into
Conversation
Item 1 — delete the 3 dead `logger` omission entries ---------------------------------------------------- PORT_SIGNATURE_OMISSIONS.md: 206 -> 203 entries (measured with the gate's own `parse_omissions` from diff_port_signatures.py, not by grep). Removed: signalwire.agent_server.AgentServer.logger signalwire.core.skill_manager.SkillManager.logger signalwire.skills.registry.SkillRegistry.logger Why each removal is correct: per the owner ruling of 2026-07-24 (ALLOWLIST_DISCIPLINE.md §8, implemented at porting-sdk scripts/enumerate_python.py:365 `_LOGGER_FACTORY_RETURN`), logging is a MODULE-LEVEL capability. The per-instance `logger` attribute was Python's structlog idiom leaking into the enumerated surface; the oracle no longer emits it. Verified against the oracle rather than trusting the wave table: $ python3 scripts/query_signatures.py python_signatures.json search logger modules.signalwire.core.logging_config.functions.get_logger That is the only `logger` the oracle records — the module-level factory. The per-class method lists for all three classes (AgentServer, SkillManager, SkillRegistry) contain no `logger` member, so each of the three entries was paperwork against a symbol that no longer exists. They excused nothing. The capability itself is unaffected: it is signalled by the 5 module-level free functions (get_logger, configure_logging, get_execution_mode, reset_logging_configuration, strip_control_chars), which this port already satisfies. No port code changed for item 1. Item 2 — task #76 test isolation: root cause was ALREADY FIXED ------------------------------------------------------------- The premise of #76 was that tests/ConstructionReadbackTests lacks [Collection(GlobalStateCollection.Name)] while ~10 sibling classes reset the singletons it reads. No [Collection] is added, and no test is serialised: the defect was in the singleton, not in the test's isolation, and it is fixed. The only global state ConstructionReadbackTests ASSERTS on is `Schema.Instance.SchemaPath`. Its failure mode was a null-return race in the `Schema.Instance` getter, which assigned inside the lock and then returned `_instance` OUTSIDE it — so a concurrent `Schema.Reset()` (which nulls the field under the same lock) could make a non-nullable property hand back null. That is what produced the 1-in-2008 net10.0 NullReferenceException. The getter now observes the instance UNDER the lock and returns that observation (src/SignalWire/SWML/Schema.cs:89-100). Nothing else in the class touches shared mutable state: it never references Logger or SkillRegistry at all; AgentServer.LogLevel and ConfigLoader.ConfigPaths are plain constructor-argument readbacks; and Logger.GetLogger is fully lock-protected and returns a value observed under the lock, so it is already safe against a concurrent Logger.Reset(). Adding [Collection] would have been the forbidden shortcut (RULES.md §4: isolation by SCOPING, never by taking away concurrency) AND would have masked a real product defect that any concurrent caller of the SDK could hit — this was never a test-only problem. New regression test: tests/SchemaSingletonConcurrencyTests.cs pins the fixed behaviour — 200k `Schema.Instance.SchemaPath` reads under Parallel.For against a thread calling `Schema.Reset()` in a tight loop. It deliberately carries NO [Collection] attribute, because the property it asserts (a reader is never handed null regardless of how a writer interleaves) belongs to the singleton itself and must hold under full parallelism. The test is non-vacuous: reverting the getter to its pre-fix form reproduces the original failure (net9.0: System.NullReferenceException), and restoring the fix makes it pass on all three TFMs. port_surface_native.json: provenance-hash refresh only (generated_from a97a5c8 -> 630ff16, the current HEAD); no surface change, no new names. Verification ------------ $ python3 ~/src/porting-sdk/scripts/diff_port_signatures.py \ --reference ~/src/porting-sdk/python_signatures.json \ --port-signatures ./port_signatures.json \ --omissions ./PORT_SIGNATURE_OMISSIONS.md \ --surface-omissions ./PORT_OMISSIONS.md \ --surface-additions ./PORT_ADDITIONS.md exit 0 — signatures match (1528 reference symbols, 2440 port symbols, 1486 excused divergences) $ bash scripts/run-ci.sh exit 0 — ==> CI PASS (23 gates, incl. SURFACE / LEDGER / TEST / FMT / LINT) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
…ff fold) The shared-diff ctor/dunder fold (porting-sdk #125, `_is_folded_dunder_member` in diff_port_signatures.py) excludes a `__init__` member finding while the reference publishes a `construction` entry for that class — the capability is still compared, by name, in `compare_construction`. Those ledger entries are therefore dead weight: the fold `continue`s BEFORE the excusal branch, so they were contributing zero excusals even before deletion. Removes the 55 `PORT_SIGNATURE_OMISSIONS.md` entries the fold makes dead. 205 -> 150 entries (337 -> 282 lines). No source changes; no allowlist, omission, or divergence entry added — deletion only. Two `__init__` entries are deliberately KEPT because the fold does not cover them (their classes are absent from the reference `construction` node, and in fact from the reference oracle entirely — they are port-only ADDITIONS, so the member entry is the only check they have): signalwire.core.agent.tools.inferred_schema.InferredSchema.__init__ signalwire.swml.verb_info.VerbInfo.__init__ Construction node unchanged: 169 classes before and after. Merge order: porting-sdk #125 FIRST. Until it merges (or PORTING_SDK_REF is pinned), this PR's CI is red BY DESIGN — the fold and the prune are mutually dependent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
mjerris
force-pushed
the
wave6/ctor-dunder-fold
branch
from
July 27, 2026 14:11
6b04506 to
14021bc
Compare
…SpiderSkill.remove_xpaths)
The signature oracle (porting-sdk d7c859d) now records 7 DERIVED public
__init__ attributes as contract — caller-observable VALUES, per
ALLOWLIST_DISCIPLINE.md class B2 (ruled 2026-07-27). dotnet already carried
two of them (SignalWireRestError.RequestId, Relay Action.Completed); the other
five were genuinely missing. Idiom: C# properties, folded by the enumerator's
accessor rename, so no allow-list or omission entry is involved.
SWMLService (SignalWire.SWML.Service) — ssl_enabled / ssl_cert_path /
ssl_key_path / domain:
Added SslEnabled / SslCertPath / SslKeyPath / Domain, hoisted off `Security`
in the ctor exactly as the reference does
(`self.ssl_enabled = self.security.ssl_enabled`, …). Settable so the caller
can override before serving, mirroring the reference's
`serve(ssl_enabled=…, domain=…)` assignment onto self.
This also fixes a real defect. `SslSettings.FromEnvironment()` re-read
SWML_SSL_ENABLED / SWML_SSL_CERT_PATH / SWML_SSL_KEY_PATH straight from the
environment at serve time, bypassing `Security` — so a cert/key/enable
supplied by the service's CONFIG FILE was silently ignored and the server
came up plain-HTTP. SecurityConfig applies defaults → env → config file (the
config file being highest priority), so serving off the hoisted properties is
a strict superset of the old env-only read. Replaced with
`SslSettings.FromService(this)`.
SpiderSkill — remove_xpaths:
Added RemoveXpaths, prefilled with the same seven expressions the reference
sets in __init__ (//script //style //nav //header //footer //aside
//noscript). It is load-bearing, not decorative: StripHtml now drops each
selected element WHOLE (element + inner text) before tag stripping, mirroring
the reference's `for xpath in self.remove_xpaths: … elem.drop_tree()`.
Previously only script/style were stripped via two hardcoded regexes, so
nav/header/footer/aside/noscript text leaked into scraped output. The `//tag`
form is honoured by the regex stripper; a richer expression is skipped rather
than mis-applied.
Tests (4 new, all wire-behavioral — each verified to FAIL under a deliberate
mutation of the code it covers, so none is vacuous):
- SWMLServiceTests: defaults mirror Security; hoisting from env; hoisting
from the CONFIG FILE (the path the old serve code ignored); caller override.
- SkillsTests: RemoveXpaths is prefilled with the reference seven; the
defaults strip their elements' text end-to-end through the scrape_url tool
against a loopback HTML fixture; and mutating the list changes what is
stripped (remove //nav → NAVJUNK appears; add //blockquote → QUOTETEXT
disappears).
Signature drift 27 → 22. The remaining 22 are all RestClient.<resource>
accessor drift from a separate task, untouched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…ing-sdk 387667e) 387667e projects the derived caller-observable attrs into python_surface.json as well as python_signatures.json, so the surface axis now records the four SWMLService TLS attrs and SpiderSkill.remove_xpaths. dotnet's apply_member_allowlist is already oracle-gated, so the regen picks them up with no enumerator change: SWMLService gains domain/ssl_cert_path/ssl_enabled/ ssl_key_path and SpiderSkill gains remove_xpaths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…rt-reuse race I briefed MY DIAGNOSIS WAS WRONG AND IS WITHDRAWN. I briefed this as ephemeral-port reuse (PickFreePort releases, another caller wins the port before the spawn). The lane measured instead of implementing, and three findings kill that theory: - macOS assigns bind(:0) MONOTONICALLY over 49152-65535. Over 40,000 allocations, min_reuse_distance=15350 — a port only returns after wrapping the whole range. - A concurrent probe (9,600 allocations, ports held, 40ms window): duplicate_handouts=0, bind_failures=0. - DECISIVE, and I could have checked it in one read: MockTest.EnsureServer is a DOUBLE-CHECKED-LOCKED SINGLETON (tests/MockTest.cs:17 — `if (existing != null) return`, then lock, then re-check). PickFreePort runs ONCE PER PROCESS. A TOCTOU race needs two racing allocators; there is one. The theory was structurally impossible. ACTUAL ROOT CAUSE: MockTest was missing the reuse guard RelayMockTest already had — whose own comment (tests/RelayMockTest.cs:585) records this exact failure causing "122 cascading Connection refused". run-ci.sh picks the port, spawns the mock, exports MOCK_SIGNALWIRE_PORT; MockTest then used a single 2-SECOND probe, so under load one slow connect made it fall through and SELF-SPAWN onto the gate's already-occupied port. The real mock exits 3 with "[Errno 48] address already in use" — AFTER printing a reassuring "listening on http://..." banner. Every REST test then hits a dead endpoint. FIXES: - tests/MockTest.cs — portFromEnv reuse guard: an explicit port PROMISES a mock, so poll and FAIL LOUD; never self-spawn onto it. - MockTest.cs / RelayMockTest.cs — self-spawn now RESERVES the port (listener held), releases only immediately before the child starts, and RETRIES on a fresh port when the child loses the bind. Plus IsAddressInUse and a WaitForExit() drain, since the bind error is written AFTER the banner. - src/SignalWire/Web/WebService.cs — SHIPPED SDK code had the same shape; ephemeral binds now retry, while an explicit caller-chosen port still throws as-is. - tests/PortReservationTests.cs — 9 new tests (verified: 9 passed). MUTATION (banner trusted as success — the real trap): RED on all three TFMs. The failure output showed stderr containing ONLY the banner with [Errno 48] not yet flushed, which is exactly why both the drain and the error-keyed detector are needed. LANE SELF-CORRECTION WORTH KEEPING: its FIRST regression test PASSED under mutation — worthless, because on a monotonic allocator a collision can never be observed from inside the allocating process. It was replaced with one that spawns the real mock onto an occupied port. A test that cannot fail is not evidence. Baseline 2016 x3 green -> 2025 x3 green, exit 0. Lint 0, formatter no-op. DRIFT byte-identical: exactly 22 RestClient.<resource>; port_signatures.json and port_surface.json untouched. PRE-EXISTING, NOT FROM THIS CHANGE: - BEHAVIORAL:SECURE-DEFAULT — tools/DumpCorpus/SecureDefaultDump.cs last changed in Wave 1 (#65), but porting-sdk carries a later checker (faae8ce) that tightened the requirement. A cross-repo gate/port mismatch, NOT papered over; needs a decision. - ReconnectTeardownMockTest lost 1/2025 on net10.0 under full-suite load (RELAY error -32602 at /tap), passing 6/6 in isolation. Same DisposeAsync family as task #32; this diff touches no reconnect/tap/dispose code. Deserves its own lane. Refs #92
…ssors The 22 `RestClient.<resource>` drifts on both the SIGNATURE and SURFACE axes were an ENUMERATOR BLIND SPOT, not a missing feature. .NET wires the namespace accessors by INHERITING the generated tree (`RestClient : Namespaces.Generated.ResourceTree`, src/SignalWire/REST/RestClient.cs:27), exactly as the Python reference's `RestClient` inherits the private `_GeneratedResourceTree`. Neither enumerator could see them: * SIGNATURES — `scripts/SignatureDump/Program.cs` reflects with `BindingFlags.DeclaredOnly` (correct for the surface: an inherited method is not re-declared surface), so the dump recorded only `RestClient.__init__`. * SURFACE — `parse_cs_file` enumerates DECLARED members and the generated-REST branch skips `ResourceTree` as a class absent from the generator manifest, so the accessors were emitted nowhere. Fixed at the enumerators, mirroring the reference enumerator's `_wired_base_attributes` (porting-sdk/scripts/enumerate_python_signatures.py): lift the members a class reaches through a NON-EMITTED generated base onto the subclass. Both lifts are scoped structurally — only bases under the generated REST namespace that the manifest does not emit as their own oracle class, and (signatures) only properties whose type resolves to a `class:` ref. Measured: the lift applies to exactly one class (RestClient) and exactly 22 members. No omission/addition/allow-list entry was added; the three ledgers are untouched. Symbol delta on both axes is exactly +22, -0. Runtime proof (the accessors were verified reachable BEFORE touching the enumerators): tests/RestMock/RestClientTreeAccessorMockTest.cs exercises `client.Calling` / `client.Fabric` / `client.Video` off a real authenticated RestClient against the shared mock_signalwire server and asserts the request lands on the right route, plus all 22 accessors resolve non-null. It reaches the mock through the public transport-injection ctor and a host-rewriting DelegatingHandler, so the SDK's own path composition, auth and resource classes all run for real. 26/26 pass on net8.0, net9.0 and net10.0; non-vacuity confirmed by a negative control (the mock journal reports `/api/video/rooms`).
…eb_hook_url TWO defects, both present, fixed together because either alone leaves the port broken. 1. THE BRIEFED ONE. AgentBase.cs:2163-2170 emitted BuildSwaigWebhookUrl(headers, token) unconditionally in the else branch. With token == null (an insecure tool) that put a tokenless, function-specific callback on the wire — no equivalent of the reference's `elif token or _swaig_query_params`. Now the reference's exact three-way branch: external _webhookUrl wins -> else local URL only when a token or _swaigQueryParams exist -> else NO key at all. 2. dotnet emitted NO SWAIG.defaults BLOCK. AgentBase.cs only ever set functions (:2185), native_functions (:2191), includes (:2197) and mcp_servers (:2203) — the same shape go had. Confirmed on the wire: the first post-guard dump carried no `defaults`, so the guard alone would have left every insecure tool with no reachable callback. Added at :2187-2200 per agent_base.py:1108-1113. A TRAP WORTH PROPAGATING: SwmlRenderer.cs:81 DOES contain a swaig["defaults"] line, but that class has zero non-test callers — AgentBase never routes through it. A grep for "defaults" finds :81 and looks green while the live agent path emits none. Check the path the agent actually renders through, not just any match. dotnet is the FOURTH of six affected ports confirmed to have BOTH defects (after go, perl, php). The gate cannot see defect 2 — php proved it stays green with `defaults` removed — so every one of these would have shipped green with unreachable insecure tools. VERIFIED INDEPENDENTLY (orchestrator): diff_port_secure_default.py --port dotnet -> EXIT 0, "✓ PASS — dotnet". Lane verification: TWO mutations, each restored. Guard -> `else if (true)`: test red on all 3 TFMs and the gate reproduced the briefed signature verbatim (has_own_webhook: true vs oracle false). Defaults block deleted (an if(false) variant failed to compile with CS0162, so deletion was the valid mutation): KeyNotFoundException 'defaults'. Both halves independently load-bearing. Full suite 2053/2053 on net8.0/net9.0/net10.0 serial; ReconnectTeardownMockTest did not recur. SURFACE suite exit 0, all 9 rules PASS — no drift regression on either axis. Two pre-existing tests RETARGETED, NOT WEAKENED: RenderSwml_WithTools asserted web_hook_url was PRESENT on a tokenless render — corrected to assert absence. ManualProxyUrl_UsedInWebhook read functions[0]["web_hook_url"] — now renders WITH a call_id so the proxy-composition assertion still bites. Not in this commit: src/SignalWire/Security/SessionManager.cs, scripts/token-interop-mint.sh and tools/TokenInteropMint/ are a concurrent TOKEN-INTEROP lane's work in this shared checkout. port_surface_native.json carries only a provenance-hash refresh from running the suite; left out as regenerable. Refs #95
…ence parity) The reference's SessionManager defaults token_expiry_secs to 900 — 15 minutes (core/security/session_manager.py:30, docstring :35 "Seconds until tokens expire (default: 15 minutes)"). This port defaulted to 3600, so a SessionManager built without an explicit lifetime minted tokens valid four times longer than every conforming implementation. Owner ruled 2026-07-27 to fold to 900; four ports (ruby, java, rust, dotnet) diverged, five already matched. Scoped strictly to SessionManager. AgentBase.TokenExpirySecs stays 3600, which is NOT a divergence: the reference's agent_base.py:130 also defaults to 3600 and passes it down explicitly (agent_base.py:247). The two defaults legitimately disagree, so the const's XML doc now records that pairing to stop a future reader "fixing" the discrepancy in the wrong direction. DefaultExpiry is a public const, which C# inlines into referencing assemblies at compile time, so the literal is itself part of the contract. Every reference was checked: the explicit `new SessionManager(3600, ...)` call sites (the wire-crypto corpus dump and three round-trip tests) pass their value in and are unaffected; the TOKEN-INTEROP mint fixture passes 900 explicitly and the checker only asserts expiry > now, so it measures the same property before and after. Tests assert the new default three ways — the constructor, the const itself, and the lifetime actually baked into a minted token's expiry field, since a stored value that never reaches the wire proves nothing. Reverting the const to 3600 turns the first and third red on all three TFMs. port_signatures.json regenerated (one line, 3600 -> 900). The signature differ deliberately ignores default VALUES, so this was invisible to the gate; the file was simply stale against its own source. port_signatures.baseline.json keeps 3600 — it is the frozen release-history snapshot SEMVER-DIFF compares against. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…ss (27 findings)
porting-sdk 90164e9 unified the drift checker to compare `type`, `kind`,
`required` and `default` on ALL parameters, not just `__init__` ones. That
surfaced 27 dotnet divergences: 13 required-flip, 11 default-mismatch,
3 default-invented. All 27 were REAL port divergences — the C# source
disagreed with the reference — so all 27 are fixed at the source. Drift for
these three kinds: 27 -> 0, with zero new drift introduced (total 374 -> 339;
the 8 extra resolutions are param-mismatch entries the same fixes cleared).
Wire bug found and fixed
------------------------
`AgentBase.EnableDebugEvents` took `string level = "all"` and emitted
`ai.params.debug_events` as a string. The reference is
`enable_debug_events(level: int = 1)` and emits `ai.params.debug_webhook_level`
as an INT (signalwire/core/agent_base.py). Both the key and the value type
were wrong on the wire. The two tests covering it encoded the old contract and
have been corrected; a third pins that nothing is emitted unless enabled.
required-flip — the reference defaults it, so the port must too
AgentBase.AddAnswerVerb(config) now `= null` (empty config)
ContextBuilder.CreateSimpleContext(name) now `= "default"`
AgentBase.AddLanguage trailing 5 params now default null
Service.OnFunctionCall(rawData) now `= null`
Service.RegisterRoutingCallback(path) now `= "/sip"`; the callback and
path parameters were also in the REVERSE order from the reference, so the
argument order is corrected too (callers updated).
default-mismatch — adopt the reference's value
AgentBase.EnableSipRouting autoMap false->true, path ""->"/sip"
AgentBase.EnableDebugEvents "all"->1 (see wire bug above)
DataMap.Webhook(formParam) ""->null
FunctionResult.RecordCall/Tap(controlId) ""->null
FunctionResult.ReplaceInHistory(text) null->true
FunctionResult.Pay(postalCode) null->true
Section.AddSubsection(numbered) null->false
Relay Action.WaitAsync/Message.WaitAsync int 30 -> double? null (the
reference's `wait(timeout=None)` is an UNBOUNDED wait; a 30s port default
silently abandoned a wait the reference would have kept)
default-invented — the reference REQUIRES it, so the port must too
Section.AddSubsection(title) no longer defaults to null
Call.TransferAsync(dest) `dest` added as REQUIRED (it was absent
entirely, so the required `dest` RELAY param could not be sent)
Client.ExecuteAsync(parameters) no longer defaults to null
CS1763 and the [DefaultValue] mechanism
---------------------------------------
Two of the defaults cannot be written in a C# signature at all: CS1763 forbids
a non-null compile-time default on a reference-typed parameter other than
string, so the reference's `postal_code: bool | str = True` and
`replace_in_history(text: bool | str = True)` must be declared `object? x = null`
and resolved in the body. The semantic default is declared with the BCL's
[DefaultValue] attribute — the standard .NET mechanism for exactly this — and
scripts/SignatureDump reads it, so the oracle records the default the caller
actually observes. The attribute is used at exactly those two sites.
Verification
------------
- tests/ReferenceDefaultsTests.cs (new, 17 tests, parallel-safe): every
behavioural test calls with the argument OMITTED, so it genuinely covers the
default rather than a passed value; required-ness is pinned reflectively
because no runtime call can assert a compile-time property.
- Mutation-tested one representative per kind (required-flip/default-mismatch,
default-mismatch, default-invented): each reverts RED and restores GREEN on
all three TFMs (net8.0/net9.0/net10.0).
- port_signatures.json regenerated and committed.
…, 2 real divergences revealed and fixed
Wave A of the omission-retirement campaign, dotnet's 5 `overload`-rationale entries.
An omission makes the drift checker STOP COMPARING a symbol; the fix belongs at the
emitter, where comparison keeps running.
Two enumerator defects, both the same shape as java ea7e0ba: the adapter was reading
ONE member of an overload set and recording it as the whole contract.
1. REFERENCE-DIRECTED UNION ACROSS OVERLOADS (_merge_overload_param_unions +
_merge_overload_return_union). C# expresses a reference `Union[A, B]` parameter the
only way a statically typed language can — one overload per arm. Both are public,
both emit the arm the reference emits, so the CAPABILITY is complete; but dedup kept
exactly one, and the recorded surface claimed the port accepts only that arm.
The merge is gated three ways and is not "two overloads means union":
* the REFERENCE must record a multi-member union at that position;
* the two overloads must actually disagree;
* the port's combined arm set must be a SUBSET of the reference's — an overload
taking a type the reference does not accept stays reported as drift.
Gate 1 is what keeps FunctionResult.RecordCall/Tap untouched: the reference types
format/direction/codec as bare `string`, so those positions compare equal today via
the existing _oracle_alignment_score and must keep doing so. Verified: 17 equal-arity
overload groups exist in the dump; exactly 3 positions folded.
2. A PROJECTION MUST NOT OVERWRITE A REAL CLASS'S OWN SIGNATURE. MIXIN_PROJECTIONS
replicates AgentBase's methods onto each Python mixin module. enumerate_surface.py
unions there (`existing | set(present)`) because PromptManager and ToolRegistry are
REAL C# classes that are ALSO projection targets; the signature side used `.update()`
and silently replaced their own signatures with AgentBase's delegating forms. The
recorded surface therefore claimed PromptManager.DefineContexts takes no argument
(it takes `object contexts`) and hid ToolRegistry's real DefineTool/RegisterSwaigFunction
entirely.
Retiring #2's blind spot revealed two REAL divergences in ToolRegistry, both fixed to
match the reference (registry.py:36) rather than re-excused:
* DefineTool defaulted `parameters` and `handler` to null, so a .NET caller could
register a tool with no schema and no handler — a definition the reference cannot
produce. Both are now required, and the method returns void, not the stored dict.
* RegisterSwaigFunction likewise returned the stored dict where the reference returns
None. No caller consumed either return value.
Four omissions retired (the comparison now runs on all four):
FunctionResult.remove_global_data union<string,list<string>> compares equal
FunctionResult.remove_metadata union<string,list<string>> compares equal
PromptObjectModel.add_pom_as_subsection union<string,Section> compares equal
PromptManager.define_contexts real signature now recorded
THREE rationales described behaviour the code does not have, and are corrected in place:
* add_pom_as_subsection claimed ".NET takes target as a string section title" — the
Section overload has existed all along (PromptObjectModel.cs:550).
* both get_basic_auth_credentials entries claimed ".NET overload returns either
(user,password) or (user,password,source) tuple union". There is no such overload
and C# cannot declare one; the port ships two separately-named methods.
* PromptMixin.define_contexts claimed a union "depending on overload". The C# side is
a single zero-arg method; there is no second overload.
Three entries stay, with rationales that now state the actual mechanism:
* AuthMixin/SWMLService.get_basic_auth_credentials — `impossible:`. A C# method's
STATIC return type cannot depend on an argument's VALUE, and C# has no literal
types, so a GetBasicAuthCredentials(bool) overload would return the 3-tuple even
when passed false — inventing a shape the reference never returns. Same ceiling in
go/rust/java/php/cpp; only perl (dynamic return) and TypeScript (literal-typed
overloads) can express the reference form. This is a genuine language limit of the
CS1763 class, not idiom.
* PromptMixin.define_contexts — a missing capability, stated as such: AgentBase's
zero-arg DefineContexts cannot reach the set-from-dict arm, and closing it needs a
dict->ContextBuilder import path ContextBuilder does not have. PromptManager's copy
DOES implement it and now compares.
Measured as SETS, not counts, with --omissions passed on both sides:
drift 150 -> 150 (introduced set EMPTY; no finding moved or regressed)
excused 1266 -> 1262
ToolRegistry.define_tool alone: 6 excused findings -> 1 (the residual is the typed-
delegate vs bare-Callable arity difference already covered by the file's header).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…ename omissions retired
All 6 rename/analog-rationale entries in PORT_SIGNATURE_OMISSIONS.md were LIVE,
suppressing 7 findings. Each claimed the divergence was "only the documented
Event-vs-RelayEvent class rename". Reading the source showed that premise is
false: the port ships BOTH classes in signalwire.relay.event —
* RelayEvent (Events.cs:19) has the reference's EXACT member set
(__init__/event_type/params/call_id/timestamp/from_payload) plus the 23
typed subclasses. It is the reference's twin, already 1:1.
* Event (Event.cs:8) is a separate raw dispatch envelope with no reference
counterpart, used by Client/Call/Message/Action to route a frame.
So this was never a rename — Call.wait_for* and Call.on were returning and
accepting the WRONG one of two real classes. Fixed at the source rather than the
rename table:
* WaitForStateAsync + WaitForAnswered/Ringing/Ending/Ended/WaitForAsync now
return RelayEvent, projected from the dispatch Event via ToRelayEvent
(event_type/params/call_id/timestamp — the same fields the reference's
RelayEvent.from_payload extracts).
* Call.On(string, Action<RelayEvent>) now takes the typed event and returns
void, matching the reference (which returns None) and the rest of the fleet.
* WaitForAsync gained the reference's `predicate` parameter. It previously
took (state, timeout) and specialised to state-waiting; the reference is
wait_for(event_type, predicate, timeout) for the general event-await. The
signature now matches the oracle exactly, param-for-param.
REAL CAPABILITY GAP CLOSED: the predicate-filtered general event wait did not
exist in this port. Two tests added covering it (no-predicate resolves on first
matching event; predicate skips non-matching ones).
excused 1229 -> 1222 (all 7 suppressed findings retired)
drift 150 -> 151
The one remaining finding is Call.on param[1]: the oracle records the handler as
class:signalwire.relay.call.EventHandler, but EventHandler is a module-level TYPE
ALIAS (Callable[[RelayEvent], Coroutine|None], call.py:56), not a class — it
appears in NO class table, so the oracle emits a dangling class: ref. Every port
that expands it to the callable form drifts; 8 of the 9 non-reference ports carry
an omission on this exact symbol. That is an oracle-fidelity fix, not a per-port
one, and creating an omission is not an agent decision — left for a ruling.
Also corrects PORT_ADDITIONS.md:195, whose rationale claimed "Python ships
RelayEvent under signalwire.relay.constants": the reference ships RelayEvent in
signalwire/relay/event.py and constants.py declares no classes at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…ntials
porting-sdk dcff742 resolved the two FastAPI credential models out of the
framework and into the reference oracle as REAL classes on
signalwire.core.auth_handler:
BasicCredentials -> username, password
BearerCredentials -> scheme, credentials
dotnet carried NEITHER. Both artifacts had `grep -c Credentials` == 0, so
against the pinned oracle the port showed 4 HARD signature drifts
(BasicCredentials.username/password, BearerCredentials.scheme/credentials, all
`missing-port`) and 6 SURFACE `unexcused_missing` — the two classes plus their
four fields. SURFACE-DIFF was red on this alone.
Two PORT_SIGNATURE_OMISSIONS entries were suppressing the consequences:
verify_basic_auth — ".NET takes (username, password) … same credential
check, different binding"
verify_bearer_token — ".NET takes the token as a bare string … the .NET layer
is framework-agnostic"
Both rationales are idiom, not a technical limit, so neither was ever a valid
omission — and the carriers are not framework-bound in the first place. They are
two-string value objects; only `flask_decorator` / `get_fastapi_dependency` are
genuinely framework-shaped, and those omissions stay.
Expressed the contract instead of excusing it:
* `BasicCredentials` / `BearerCredentials` as sealed records with explicit
init-only properties — the .NET idiom for the reference's pydantic models.
* `VerifyBasicAuth(BasicCredentials)` / `VerifyBearerToken(BearerCredentials)`
now take the single credentials object the reference does.
* `VerifyBearerToken` compares ONLY `Credentials`; `Scheme` is carried but not
part of the comparison, matching auth_handler.py:113-119. Requiring
scheme == "Bearer" would reject requests the reference accepts, so that is
pinned by a test.
* Both omission entries deleted — the port carries the real shape.
Enumerator: registered both classes in CLASS_MODULE_MAP (shared by the surface
and signature enumerators), and folded `__init__` out of the SURFACE projection
via SURFACE_METHOD_ALLOWLIST — the reference records only the two fields for
these dataclass-style models, exactly as the AI-Chat response records already
do. The allowlist unions the oracle's own member set first, so the entry
self-retires if the oracle ever records `__init__`.
Measured against a scratchpad-pinned oracle (md5 7cb4b078b5b7da349ae686e24d084813
signatures / d15aced4750100a0016946ede1b85a63 surface, identical across both
arms), as SETS:
DRIFT 5 -> 1 4 resolved, 0 introduced
EXCUSED 1372 -> 1368 4 resolved, 0 introduced
SURFACE unexcused_missing 6 -> 0 unexcused_extra 0 -> 0
The one remaining drift (`Call.on` handler type) pre-dates this change and is
untouched by it.
Tests: 2077/2077 pass on net8.0/net9.0/net10.0 (2075 before + 2 new — the
scheme-ignored behaviour and the carrier field names).
Coordinated-With: porting-sdk dcff742
…omissions retired
The two RelayClient EventHandler-family entries in PORT_SIGNATURE_OMISSIONS.md were
not retirable as dead paperwork: their rationale was inverted, and the divergence
they hid was real.
Rationale claimed: ".NET RelayClient.OnCall/OnMessage returns a typed handler
delegate (CallHandler/MessageHandler) for unsubscribe support; Python returns the
RelayClient itself for fluent chaining."
What the source actually does — the exact opposite on both halves:
* Python (relay/client.py:327) `def on_call(self, handler: CallHandler) ->
CallHandler` returns THE HANDLER (decorator form, `return handler`).
* C# returned `Client` (`return this;`) — the fluent-chaining return the
rationale attributed to Python.
* No CallHandler/MessageHandler delegate type existed in the C# source at all.
Retiring the entries unchanged took drift 0 -> 4: the port also took a second
handler argument Python does not have (`Func<Call, Event, Task>` vs
`Callable[[Call], ...]`). That was an invention, not a ceiling — Python invokes
`self._on_call_handler(call)` with ONE argument, java's RelayClient.onCall takes
`Consumer<Call>` (also one), and 0 of the 26 in-repo call sites referenced the
Event parameter.
So the fix is in the port, not the ledger:
* `OnCallHandler`/`OnMessageHandler` are now `Func<Call, Task>` /
`Func<Message, Task>`.
* `OnCall`/`OnMessage` return the handler, mirroring Python's decorator form.
* 27 call sites + 12 doc snippets updated; README include re-synced to its
QuickstartRelay.cs fixture region.
Measured against a pinned oracle (python_signatures.json md5
7d9f2fa2e1f385df2afac88b657a6475, python_surface.json md5
5e5451c050e10c438a1b8b307de6e703), identical in both arms:
arm drift excused
baseline (entries present) 0 1401
entries removed, port unfixed 4 1397 <- anti-template
entries removed, port fixed 0 1397 <- shipped
Excused set delta is exactly {RelayClient.on_call, RelayClient.on_message}
leaving; nothing entered, drift stayed empty.
… record its provenance
Owner ruling 2026-07-28: every port declares 3.0.0, and the release floor
(port_signatures.baseline.json) is re-anchored to that wave.
Two halves, both required. Setting baseline_version alone does NOT satisfy
SEMVER-DIFF: the gate does not compare version strings, it DIFFS the current
surface against the floor's recorded `modules` payload and sets required='major'
whenever they differ (semver_diff.py:335). Neither 'none' nor 'downgrade' can
satisfy 'major' (both rank -1 at :528). So the payload is replaced too.
src/SignalWire/SignalWire.csproj <Version> 4.0.0 -> 3.0.0
(the port's only <Version> declaration)
port_signatures.baseline.json baseline_version 3.0.2 -> 3.0.0
modules 123 -> 132 (+ construction), copied
from a FRESH regen of port_signatures.json
generated_from re-anchored to this HEAD
generated_from_commit ADDED (see below)
This is a PAYLOAD SWAP, not a file copy — the floor carries release-anchor
metadata the current artifact does not, and every pre-existing key is preserved.
Provenance anchor: dotnet's floor recorded NO baseline anchor at all, so
semver-diff emitted a wave-A report-only finding on every run — "record either a
commit SHA ('generated_from_commit') or a release tag + sha; an unanchored
working-tree snapshot is not trustworthy." Since this commit rewrites that exact
file, it records the anchor the gate asks for rather than shipping a knowingly
unanchored floor. That clears the finding: the gate's output is now the verdict
line alone.
What it means semantically: 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 — dotnet's published tags top out at v1.1.2
— so the downgrade regresses no artifact. The consequence to be explicit about
is that SEMVER-DIFF will no longer flag anything already present in today's
surface; future breaking changes are still caught, now measured against the new
floor.
This sets version INTENT only. No tag, no release. The CHANGELOG's top heading
is a historical release entry, not a declaration site, and is left as-is (same
call the typescript and php lanes made today).
Verified: semver_diff.py --port dotnet exits 0 —
[semver-diff] dotnet: 3.0.0 (3.0.0) -> 3.0.0 (src/SignalWire/SignalWire.csproj)
actual bump = 'none', required = 'none' [ok]
No SEMVER_DIFF_ALLOW.md entry was added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…o 3.0.0 Nothing above v1.1.2 was ever published, so the 3.0.2/3.1.0/3.2.0/4.0.0 headings were never a release history. Collapsed into a single 3.0.0 entry; all 13 bullets preserved including both BREAKING notes (LiveTranscribeAsync/ LiveTranslateAsync params.action, and the RelayError.Code addition). Owner ruling 2026-07-28. Fixes META-CONSISTENT version-vs-changelog.
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
Base64UrlEncode ended with .TrimEnd('='). The reference mints with
base64.urlsafe_b64encode, which KEEPS the '=' padding, and validates with
base64.urlsafe_b64decode, which RAISES on a stripped '='. Every token this port
minted was therefore 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.
Base64UrlDecode kept accepting them because it re-pads before decoding. That
encoder/decoder asymmetry is why round-tripping a token against ourselves could
never surface the bug, and why the new gate validates against the REFERENCE's
decoder rather than our own.
Also wires the TOKEN-INTEROP gate (property 3 of the SWAIG tool-token contract):
tools/TokenInteropMint mints one token from the fixed inputs the checker exports,
driven by scripts/token-interop-mint.sh. Per-PR, not nightly — a security property
should not wait. This is the fourth port found with this exact defect (java, perl
and cpp are the others), so the gate closes a real fleet-wide class.
Two loose ends carried in the same commit because they are one thought:
- port_surface_native.json was regenerated with the credential classes when that
surface landed but never committed, leaving the tracked artifact stale.
- tests/SchemaSingletonConcurrencyTests.cs pins that Schema.Instance never hands
back null while another thread calls Reset(). See its remarks: it is a REGRESSION
PIN on already-correct code, and it does NOT currently discriminate — it passes
against the broken getter shape too, because the constructor's cost inside the
lock closes the window. Documented as a known limitation rather than presented as
proof.
NOT included: a proposed rewrite of Schema.Instance to assign under the lock and
return the FIELD outside it. That was measured and REJECTED — it introduces the
null-return window it claimed to fix and costs every reader a lock acquisition by
dropping the existing lock-free fast path. The current getter snapshots to a local
on the fast path and returns the assignment expression's value inside the lock;
neither return can observe a nulled field.
Verified: TOKEN-INTEROP exit 0; re-introducing .TrimEnd('=') reproduces
"urlsafe_b64decode raised Error('Incorrect padding')", so the gate fails for the
right reason. net8.0 suite 2077/2077.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…esizes
SURFACE-DIFF listed 30 missing-port symbols and every single one was `__init__`
(verified: filtering the list for non-`__init__` entries returns nothing). All are
Python `@dataclass` classes — relay.event.*, ai_chat.client.{ChatLog,ChatResponse,
ConversationInfo}, core.auth_handler.{Basic,Bearer}Credentials,
rest._request_options.RequestOptions — so python_surface.json records an `__init__`
that exists only because the decorator generates it; the reference source has no
`def __init__` either.
C# expresses the same construction in two shapes this text-based enumerator cannot
see as a "constructor":
* object-initializer classes — `public sealed class CallReceiveEvent {
public string CallState { get; init; } = ""; ... }` has an IMPLICIT parameterless
constructor. `grep "public CallReceiveEvent("` returns ZERO.
* positional records — `public sealed record ChatLog(...)`, BasicCredentials,
BearerCredentials, whose canonical ctor is generated from the header.
porting-sdk 8828dd2 made emitting `__init__` mandatory fleet-wide, so all 30 went red
at once. This is the same root cause as java's and rust's, and the ports that were
already green (go/typescript/ruby/php/cpp) all emit `__init__` on these same classes —
go does it explicitly via eventTarget()'s `SyntheticMethods: ["from_payload",
"__init__"]`.
Fixed by EMISSION, not omission (RULES.md §2 — fold at the emitter, never omit): the
constructors are real and public. Entries go in the existing SURFACE_METHOD_INJECTIONS
table, which already carries exactly this kind of fold (SkillRegistry / SchemaUtils are
private-ctor singletons whose `__init__` capability is likewise real).
Verified real, not assumed: every one of the 30 resolves to a `class` or `record`
declaration under src/.
Verified: SURFACE-DIFF "✓ port matches Python reference (2576 symbols; 26 excused
omissions, 349 excused additions)".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…n uncovered The ratchet's doctrine is "drive the number DOWN; never up." Four slots closed by declaring the type the reference already declares — and closing the DataMap three surfaced a live wire divergence no gate catches. ## The wire bug (the reason this is not just a ratchet commit) `DataMap.Expression` emitted `nomatch_output`. The reference emits `nomatch-output` — HYPHENATED (data_map.py:202), and behavioral_manifest.yaml:3443 records the same `nomatch-output?`. An underscored key is one the server does not recognise, so the no-match branch of every DataMap expression silently never fired. dotnet also stored `output` raw where the reference stores `output.to_dict()`. FLEET SWEEP — this is not dotnet-only. Emitting the key: correct (nomatch-output): java, go, typescript, ruby, perl WRONG (nomatch_output): dotnet (fixed here), cpp, php, rust The other three are NOT fixed in this commit; they need the same change plus their own tests, and each is a separate port's tree. ## The erosion slots (4 closed, all "the reference declares it, so declare it") DataMap.output(result) object -> FunctionResult DataMap.fallback_output(result) object -> FunctionResult DataMap.expression(output) object -> FunctionResult (+ nomatch_output arg) Step.set_functions(functions) object -> two overloads `object` was not idiom here, it was INVENTED WIDTH: the reference's `output()` calls `result.to_dict()` UNCONDITIONALLY, so a string or dict is an AttributeError there — a call the reference cannot make. dotnet's `ResolveOutput` passed non-FunctionResult values straight through, and four tests existed only to assert that width: Output_OnWebhookWithDict, Output_OnWebhookWithString, FallbackOutput_SetsGlobalOutput, FallbackOutput_WithString. Invented surface and its tests are deleted, not excused. `ResolveOutput` itself is gone — it existed only to service the widened arm. `Step.set_functions` is a REAL union (`str | list[str]`, contexts.py:236) where "none" is a synonym for []. C# expresses that as two overloads — the same pattern ec53a83 established for reference-directed unions — not as `object`. Both arms serialize through the same field, so the wire shape is unchanged. Expression_WithNomatchOutput ASSERTED THE BUG (`nomatch_output`, raw string value). It now asserts the reference contract and that the underscored key is ABSENT, so the bug cannot come back green. CreateExpressionTool likewise. The two factories accept either spelling on INPUT (their dictionaries are caller-supplied) but always EMIT the hyphenated key. Also re-drifted: port_signatures.json + port_surface_native.json regenerated (the tightened types are surface), and the ratchet lowered 19 -> 16 in run-ci.sh. ## What is NOT fixed, and why the remaining 16 is not 16 erosions 9 of the residual findings are the differ aligning POSITIONALLY past a parameter the port is MISSING: it keys on (class, method, index), so the port's trailing CancellationToken lands where the reference has a real typed param. HttpClient.post declares `RequestOptions? requestOptions` perfectly and is still listed. Those need parameters added, not types tightened — a different job, deliberately not bundled here. Verified: dotnet test --framework net8.0 -> 2073 passed, 0 failed (exit 0) diff_port_type_erosion --max 16 -> exit 0 (was 20 slots, now 16) run-format.sh -> exit 0 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 16 -> 9 (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 dotnet --repo . --max 9 -> exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
dotnet's `LoggingConfig.StripControlChars(Dictionary<string, object?>)` already
matched the reference's contract exactly — event dictionary in, string values
scrubbed, non-strings passed through — and it had three passing tests for that
contract. It also had ZERO call sites: grep the tree and the only non-test hit is
its own definition. `Logger.Log` interpolated the caller's message straight into
the line it wrote to Console.Error, so a NUL, a BEL, or an ESC-[ escape reached
the terminal intact and could forge log lines. The reference registers this scrub
in BOTH of its structlog processor chains (logging_config.py:205,233); here it was
a method nobody invoked.
StripControlCharsValue NEW internal per-value scrub (the unit a line-oriented
emitter needs; StripControlChars now delegates to it, so
the two can never diverge). `internal`, not `public` —
the port surface is unchanged.
Logger.Log now scrubs before writing to Console.Error
THE CONTRACT TESTS WERE ALREADY GREEN AND STAYED GREEN through the break — three
of them, asserting the dictionary transform, passing for the life of the port while
every emitted log line went out unscrubbed. A correct signature was never what
stood between a caller and a forged log line.
The new WIRING tests drive the real Logger and read what it actually wrote, so
removing the scrub turns them RED on all three target frameworks:
SignalWire.Tests.LoggerTests.LogOutput_HasControlCharsStripped [FAIL]
Assert.DoesNotContain() Failure: Item found in collection
Failed: 1, Passed: 23 — net8.0, net9.0 and net10.0 alike
A test calling StripControlChars directly passes against that same break.
PARALLEL-SAFETY: Console.SetError is process-global, so the capture helper is only
sound because LoggerTests already sits in GlobalStateCollection — xUnit runs that
collection without a concurrent sibling. The isolation comes from the existing
scoping, not from disabling parallelism, and the original writer is restored in a
finally.
The control characters are written as backslash-u escapes, not raw bytes. That is not cosmetic: embedding them literally makes git classify the
whole file as binary (`Bin 6029 -> 8362 bytes`, no reviewable diff).
Also 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, 2075 tests PASS on each of net8.0/net9.0/net10.0.
run-lint.sh (AnalysisMode=All, TreatWarningsAsErrors) 0 warnings, 0 errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…wo false omissions
SNIPPET-COMPILE failed on one snippet, and the ledger entries that made it look correct
were false. Both are the same defect at two layers, so they land together.
THE DOC (docs/swml_service_guide.md:132) called RegisterRoutingCallback("/custom-endpoint",
handler) — path-first, which is CS1503 against the real signature:
src/SignalWire/SWML/Service.cs:347
public void RegisterRoutingCallback(
Func<Dictionary<string, object?>?, Dictionary<string, string>, object?> callback,
string path = "/sip")
That is callback-first WITH the reference's own "/sip" default, matching
core/swml_service.py:918 and core/mixins/web_mixin.py:1281. dotnet already agreed with the
reference; only the doc was wrong.
The snippet also returned `new { status = "ok", timestamp = DateTime.UtcNow }`, which
compiles against object? but teaches the wrong contract — swml_service.py:930-933 specifies
the callback returns a route string to redirect to, or null to continue normal processing.
Replaced with a route-or-null body matching the real call sites (AgentServer.cs:214,
SwmlServiceAiSidecar.cs:99). ExtractSipUsername was verified as real surface first
(Service.cs:1108).
THE LEDGER carried the fabricated signature that the doc was written against:
PORT_SIGNATURE_OMISSIONS.md:134 web_mixin.WebMixin.register_routing_callback
PORT_SIGNATURE_OMISSIONS.md:148 swml_service.SWMLService.register_routing_callback
both claiming ".NET takes (path, callback_fn) ... parameter order swap". No swap exists.
An omission excuses the WHOLE symbol from comparison, so these were permanent blind spots:
a future real change to either symbol would have gone unseen.
Both lines are DELETED OUTRIGHT, not commented out. A tombstone would keep the symbol name
and the fabricated "(path, callback_fn)" text in the file, so `grep register_routing_callback
PORT_SIGNATURE_OMISSIONS.md` would still hit and an audit of "which symbols are excused"
would get a false positive — and the bad pattern would still be greppable for someone to
re-derive. Git history is where the record belongs.
This is a propagation, not two coincidences: go's own ledger cited THIS dotnet entry as
corroboration for an equally false claim about its own code (deleted in go 63eb963). One
fabricated rationale became the citation that justified the next.
Verification:
snippet_compile.py --port dotnet --repo . -> exit 0
"dotnet: clean (219 compiled)" (was: CS1503 at docs/swml_service_guide.md:132)
sw-verify dotnet --gates SURFACE,LEDGER -> exit 0
SURFACE PASS · LEDGER PASS · NO-LAUNDER PASS
(drift unchanged by the deletions — the port genuinely matches)
run-format.sh -> exit 0, changed nothing
Swept md/cs/fs/vb: that doc block was the only path-first occurrence; all 7 real call
sites in tools/tests/examples/src were already callback-first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…rship
ConcurrentAcquisitions_EachCallerStillOwnsItsPortOnReturn was failing. The assertion was
wrong, not merely unlucky.
`Assert.Empty(duplicates)` (line 120) asserted a CUMULATIVE claim — that no ephemeral port is
ever handed out twice for the whole life of the test. That claim is false, and it should be.
Once a listener closes, its port returns to the kernel's ephemeral pool and being re-handed is
CORRECT behaviour, not a defect.
The recycling comes from inside the test: 32 workers each reserve 40 ports and release all 40
at the end of their loop, so fast workers free 40 ports while slower workers are still
reserving. Linux draws from 32768-60999 randomly and recycles immediately; macOS walks
49152-65535 monotonically, which is the only reason the bad assertion appeared to hold there.
The invariant the test exists to guard — a port is never handed to a second caller WHILE THE
FIRST STILL HOLDS IT — was never violated. Instrumented on Linux: of 456 sampled "duplicates",
456/456 had the earlier holder ALREADY CLOSED and 0 were two live listeners. `notOwned` was
empty in every run. The test was failing on correct kernel behaviour.
FIX: `handedOut` (cumulative, never shrinks) becomes `outstanding` (ports currently reserved),
with each port removed from the set immediately BEFORE its listener is stopped; `duplicates`
becomes `overlaps`. The assertion now covers only reservations held at the same instant.
`Assert.Empty(notOwned)` is unchanged.
FIXED BY SCOPING, NOT BY SERIALISING. No [Collection], no mutex, no thread-count reduction —
Workers=32, RoundsPerWorker=40 and MaxParallelThreads=-1 are all untouched.
TWO CORRECTIONS TO THE RECORD:
- "1 failed of 2075, on net10.0" is misleading. The nightly's own last line is
"==> TESTS FAILED (framework(s): net8.0 net9.0 net10.0 )" — ALL THREE failed; only
net10.0's per-TFM summary survived log truncation.
- It is NOT intermittent. Deterministic on Linux (5/5 fail), 0/5 on macOS — platform-
dependent, not timing-dependent. That is why it never reproduced from a local macOS box,
and why the verification below was run in the CI Linux image under Docker.
Verification:
real test, Linux net10.0, BEFORE (fix stashed) 5/5 FAIL, same Assert.Empty() at line 120
real test, Linux net10.0, AFTER, same concurrency 10/10 PASS
full suite, Linux, 3 TFMs x 2 rounds 6/6 green, 2075/2075 each
full suite, macOS, 3 TFMs exit 0, 2075/2075 each
isolated assertion shape, Linux x30 old 30/30 fail (2628 hits) -> new 0/30
mutation (ReservePort -> pick-and-release) still RED ("Address already in use"); reverted
local re-verify (mine): dotnet test -f net10.0 --filter PortReservation -> exit 0, 9/9 passed
run-format.sh exit 0 (no-op) · run-lint.sh exit 0, 0 warnings 0 errors ·
audit_no_cheat_tests.py exit 0, clean
NOT a shared root cause with task #32. This is a pure test-assertion change touching no
RELAY/dispose code. A separate macOS net10.0 failure in
ReconnectTeardownMockTest.ServerDisconnect_ThenDisposeDuringBackoff_NoUnobservedFault appeared in
one pre-fix run (unobserved AggregateException after DisposeAsync) — different mechanism,
already flagged pre-existing in 79b4b32's trailer, and it did not recur in any post-fix run.
FLAGGED FOR REVIEW, not changed: the mutation check kills this test only 1 run in 3. That is
unchanged from the original assertion's discriminating power and consistent with the test's own
scope note (SelfSpawn_RecoversWhenTheMockLosesTheBind_... is the mutation-discriminating test).
Making this in-process test a hard gate rather than a cheap pin needs a different design.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…ce of the fleet sweep
Ninth and final port of the fleet-wide false-ledger sweep. 13 of 153 entries were suppressing
nothing; all 13 deleted outright, no tombstones.
FOUR CLAIMS IN MY OWN BRIEF DID NOT SURVIVE SOURCE:
- "~41 entries" — real count is 153 (131 signature + 22 surface). I under-counted every port in
this sweep, always low. (A naive parse says 132/27, but `Format:`, `rules:` and `stack:` are
prose-wrap artifacts that SYMBOL_RE accepts as symbols.)
- "dotnet is where 30 implicit-ctor entries were noted open under #136" — NO SUCH CLUSTER HERE.
There are exactly 2 __init__ entries (InferredSchema, VerbInfo), both genuine port-only
ADDITIONs suppressing real drift. Nothing ledger-side relates to #136.
- "*Event.from_payload is a known carrier" — ZERO from_payload entries in either file. The 24
fabricated in cpp did not replicate here.
- "SWMLBuilder.add_section/reset are known carriers" — REAL here, unlike go. Reference returns
class:Self, port returns class:...SWMLBuilder, and the rationale describes it accurately.
Both probe sig_rc=1. Untouched.
(a) FABRICATED — rationale describes a divergence that does not exist. Deleted (7):
AIConfigMixin.enable_debug_events claims ".NET takes string severity".
src/SignalWire/Agent/AgentBase.cs:1249 EnableDebugEvents(int level = 1)
ref core/mixins/ai_config_mixin.py:512 enable_debug_events(self, level: int = 1)
Byte-identical in both JSONs.
DataMap.output / DataMap.fallback_output DataMap.cs:203,210 vs core/data_map.py:334,350.
Identical.
logging_config.strip_control_chars claims ".NET takes only the payload arg" as if divergent —
the REFERENCE also takes exactly one arg (core/logging_config.py:33). Identical.
Action.wait claims ".NET takes timeout as int seconds".
src/SignalWire/Relay/Action.cs:69 WaitAsync(double? timeout = null)
ref relay/call.py:114 wait(self, timeout: float | None = None)
Both optional<float>.
MCPGatewaySkill.get_parameter_schema (surface) claims ".NET inlines" it. It does not —
McpGatewaySkill.cs:53 `public override Dictionary<string, object> GetParameterSchema()`,
and it IS in port_surface.json.
SWMLService.security (surface) AN `approved:` ENTRY WHOSE OWN "VERIFIED not a rename" CLAIM IS
FALSE. Rationale: "dotnet's SWMLService exposes NO public security/Security member of any
name." Source: src/SignalWire/SWML/Service.cs:107
`public SecurityConfig Security { get; }`, present in port_surface.json.
An approved entry asserting its own verification is the worst shape in the ledger.
(b) TOLERATED NATIVELY BY THE DIFFER — never load-bearing. Deleted (3):
Call.transfer the rationale is factually ACCURATE (port has an `extra` dict where the
reference has **kwargs — Call.cs:254 vs relay/call.py:1178) but
diff_port_signatures.py:1055-1072 tolerates extra port-side optional params
unconditionally, so the entry suppresses nothing regardless.
WebService.app, AgentServer.app `app` is in NEITHER oracle, so there is nothing to compare.
See the enumerator defect below — this one is not as clean as it looks.
(c) REDUNDANT CROSS-FILE DUPLICATES — signature copy deleted, surface copy RETAINED (3):
AgentServer.agents, SkillManager.loaded_skills, WebService.security were listed in BOTH files.
diff_port_signatures.py:1296 (is_surface_excused) makes the PORT_OMISSIONS.md entry cover the
signature check, so the signature copy was dead — but the SURFACE copy is load-bearing (all
three symbols are in the oracle, absent from port_surface.json, each probes surf_rc=1).
THIS IS EXACTLY THE PHP INTERACTION TRAP: the signature copies look like independent no-ops and
are no-ops ONLY BECAUSE the surface entry covers them. Only the covered side was deleted.
MISWORDED-BUT-REAL: none found. Action.wait was the near-miss — its rationale is false AND a
residual divergence exists underneath (reference returns RelayEvent, port records `any`) — but
diff_port_signatures.py:179 treats `any` as matching anything, so the entry still suppresses
nothing and deletion is safe. Confirmed by the negative control.
Verification — both consumers probed separately, per the cpp lane's correction that
`--gates SURFACE,LEDGER` alone is insufficient scope:
BASELINE: SURFACE PASS · LEDGER PASS · NO-LAUNDER (impossible:16 approved:6 idiom:1) · exit 0
AFTER: SURFACE PASS · LEDGER PASS · NO-LAUNDER (impossible:16 approved:5 idiom:0) · exit 0
(approved 6->5 is the false dotnet-no-public-security; idiom 1->0 is Call.transfer)
diff_port_signatures.py -> exit 0, 1393 excused divergences (unchanged from baseline)
diff_port_surface.py -> exit 0, 24 excused omissions (baseline 26)
NEGATIVE CONTROL (all entry lines stripped from both files):
diff_port_signatures.py exit 1 with 176 drifts · diff_port_surface.py exit 1 with 19 missing
symbols. The RETAINED entries have teeth.
run-format.sh exit 0 · run-lint.sh exit 0, 0 Warning(s) 0 Error(s). NO dotnet source touched.
port_surface_native.json was deliberately EXCLUDED from this commit: the gate run rewrote its
`generated_from` git-SHA stamp, but its 2374-name symbol list and every other key are
byte-identical to HEAD. Stamp churn, not a surface change.
FOR AN OWNER — ENUMERATOR DEFECT, `self.app` IS INVISIBLE. The reference genuinely has it on both
classes (agent_server.py:68 `self.app = FastAPI(...)`, web_service.py:113
`self.app: FastAPI | None = None`) and dotnet genuinely has no counterpart (it hosts on
HttpListener). So a REAL divergence exists — but `app` appears in NEITHER python_surface.json nor
python_signatures.json, so no gate can ever see it. web_service.py:113 is PEP-526 annotated and
still absent, which makes this more than "unannotated assignments are skipped". The two entries
were deleted because they suppress nothing, but the gap is real and invisible; if the enumerator
is fixed to record annotated instance attributes, these two symbols will surface as genuine drift
and will need properly-worded entries. Flagging, not fixing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
… reference
DataMap.Webhook() inserted the caller's method verbatim, so `Webhook("get", ...)` emitted
`"method": "get"` where the reference emits `"method": "GET"`.
reference signalwire/core/data_map.py:230
webhook_def: dict[str, Any] = {"url": url, "method": method.upper()}
dotnet src/SignalWire/DataMap/DataMap.cs:153 (before)
["method"] = method,
SEVERITY IS WIRE PARITY, NOT A BROKEN CALL. The engine reads the method field
case-insensitively (mod_openai/actions.c:774 uses strcasecmp; mod_infrastructure/swml_schema.c:1415
enumerates both cases), so a lower-case method still routes. What was wrong is that the same
program in dotnet and python produced byte-different SWML, and payload equality is the contract
the port matrix is measured against.
FOURTH AND LAST of the four ports carrying this — php 5d073a7, rust 0a66397, cpp 94f29b9 landed
earlier today. Task #173 is now closed.
ToUpperInvariant(), not ToUpper(): under a Turkish locale ToUpper() maps "i" to "İ", which would
make the emitted wire value depend on the machine's culture. That is a real defect class in C#,
not a style preference.
A REQUIREMENT MY BRIEF DID NOT ANTICIPATE, AND I CONFIRMED IT RATHER THAN TAKING IT ON TRUST:
calling a method on a public-API parameter trips analyzer CA1062 under this repo's
AnalysisMode=All + TreatWarningsAsErrors. I removed the guard and re-ran run-lint.sh to check:
error CA1062: In externally visible method 'DataMap DataMap.Webhook(string method, ...)',
validate parameter 'method' is non-null before using it.
— fired on net8.0, net9.0 AND net10.0.
Resolved with `ArgumentNullException.ThrowIfNull(method);`, the idiom already used 9x elsewhere in
this same file. php and rust have no equivalent analyzer, and cpp's toupper path takes a
std::string by reference, so this is genuinely dotnet-specific rather than something the other
three ports silently skipped.
No gate catches the underlying class. SIGNATURES/DRIFT compare the shape of Webhook(...), which was
always identical; only the emitted VALUE diverged, and nothing compares emitted DataMap payloads.
Verification (matched pair, real runs, all three TFMs):
BEFORE (test in place, source unfixed):
Assert.Equal() Failure: Values differ — Expected: GET, Actual: get
at DataMapTests.Webhook_UpperCasesMethodOnTheWire() in tests/DataMapTests.cs:line 227
Failed: 1, Passed: 0 on net8.0/net9.0/net10.0.
The red landed ON the new assertion, not upstream of it.
AFTER: DataMapTests 37 passed / 0 failed per TFM (was 36; +1 is the new test).
FULL SUITE: baseline with the change stashed = 2075 passed / 0 failed x 3 TFMs;
after = 2076 passed / 0 failed x 3 TFMs. Delta is exactly the one new test.
run-format.sh exit 0, changed nothing. run-lint.sh exit 0, 0 Warning(s) 0 Error(s).
port_surface_native.json untouched — no gate run rewrote its stamp this turn.
The test also pins that an already-upper-case method ("DELETE") is left unchanged, so the fix
cannot regress into double-transforming.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…at 100% dotnet was measured for the first time today at 99.0% (1289/1302). The 13 undocumented types were the only ones left; they are now documented and the floor is pinned at 100.0, so the ratchet can only hold or rise. Every comment is written from the type's own source, and records behaviour the signature does not carry — the wire shape each ToDict emits (which keys are omitted, and the one that is emitted even when false), the two exclusive-or text-vs-POM constraints, the inherit-rather-than-reset semantics that make an unset step function list surprising, the SSRF gate's blocked ranges and its env bypass, and the ai-verb object-prompt contract. Types documented: Contexts/ContextBuilder.cs GatherQuestion, GatherInfo, Step, Context Logging/Logger.cs LogLevel, Logger POM/PomBuilder.cs PomBuilder POM/PromptObjectModel.cs Section, PromptObjectModel SWML/SWMLBuilder.cs SWMLBuilder SWML/SwmlRenderer.cs SwmlRenderer Utils/ExecutionMode.cs ExecutionMode Utils/UrlValidator.cs UrlValidator Docs only — no code, signature, or behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…xistent play.text
Two real wire defects in SwmlRenderer, both surfaced by the doc burn and both
invisible to the existing tests, which only substring-matched the rendered JSON
blob (`Assert.Contains("ai", json)`) and so passed for any shape.
1. `ai.prompt` / `ai.post_prompt` were emitted as BARE STRINGS. The SWML `ai`
verb requires them to be objects — {"text": …} or {"pom": […]}. mod_openai's
app_config.c does `!cJSON_IsObject(prompt)`, fires calling.error and ABORTS
THE CALL, so this was fatal on the wire, not merely non-canonical.
The port already knew the contract and contradicted itself: SWMLBuilder.Ai
next door wraps correctly and its doc comment cites the same app_config.c
check. RenderSwml hand-assembled its own aiConfig dict and bypassed the
builder that owns the rule. Fixed by routing through builder.Ai(...) — which
is also what the Python reference does (core/swml_renderer.py:131 calls
builder.ai(prompt_text=…)), so the wrapping can no longer drift out of one
of the two paths.
`prompt` must now match the shape `promptIsPom` declares; a mismatch throws
ArgumentException rather than rendering a document whose `prompt` key
silently vanished (the renderer writes via Document.AddVerb, which does not
validate).
2. `RenderFunctionResponseSwml` emitted `play {"text": …}`. The SWML `play`
verb has no `text` key — its config is PlayWithURL/PlayWithURLS and spoken
text goes through the `say:` URL scheme. Now emits `url: "say:<text>"`,
matching the reference, which already carries this exact fix.
Adds tests/SwmlRendererWireShapeTests.cs: 13 tests that PARSE the emitted
document and assert on actual keys and JsonValueKind, covering both defects
plus the neighbouring ai keys (post_prompt_url stays a bare string, SWAIG
functions/defaults, params merging as top-level ai keys, verb ordering,
record_call) so a future hand-assembly regression is caught at the wire rather
than at construction.
Verified: run-format.sh, run-lint.sh (AnalysisMode=All, TreatWarningsAsErrors —
0 warnings, 0 errors), run-tests.sh across net8.0/net9.0/net10.0 — 2089 passed,
0 failed on each (2076 pre-existing, all green before and after).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…ed nothing dotnet and rust were the only two ports whose run-ci.sh never invoked doc_surface.py. The floor pinned in 863fc11 was therefore decoration: the gate existed and the number was recorded, but nothing ran it outside a manual invocation, so the next undocumented public type would have landed silently. Found by the rust doc-burn lane checking whether its own freshly-pinned floor was actually enforced instead of assuming it, and confirmed here. Wired BLOCKING, not report-only. dotnet is at 100.0% (1302/1302) as of 863fc11 — every public type carries an XML doc comment — so a regression is real and provable against the pinned floor. Per-PR rather than nightly: a pure text scan with no build or restore, free next to dotnet's three-TFM matrix. Verified: doc_surface.py --port dotnet --repo . -> 100.0% (1302/1302); floor 100.0%, exit 0. bash -n scripts/run-ci.sh -> syntax ok. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Regenerated by the SURFACE gate during a full run-ci; provenance line only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Preprocessor directives sit at column 0; my pragma was indented and the widened FMT gate moved it back, re-indenting the trailing comment lines with it. Third time this lane: FMT applies locally but runs --verify-no-changes in CI, so an uncommitted auto-correction is green locally and red in CI. Any hand-written #pragma needs to go in at column 0 to avoid it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Owner ruling 2026-07-30. CA2007 demands ConfigureAwait(false); xUnit1030 is "Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits". xUnit owns the synchronization context a test body runs in, so inside a [Fact]/[Theory] its rule is the authoritative one and CA2007's advice is actively harmful. Proven by OSCILLATION rather than argued: burning CA2007 to zero produced 19 xUnit1030 errors, and removing the ConfigureAwait to satisfy xUnit put the CA2007 findings straight back. No source text satisfies both. This is the THIRD analyzer-vs-xUnit contradiction in this suite — CA1707 vs the Method_Scenario naming, CA1515 vs xUnit1000/xUnit1027, and now CA2007 vs xUnit1030. Consistent pattern: where a CA rule and the test framework disagree about a TEST BODY, the framework wins, because it owns the runtime the body executes in. Burned everywhere it was NOT a test body, so src/, tools/, examples/ and the test helpers are clean and stay that way. Adding the block shifted the CA1303 disable from line 177 to 199, which is exactly the line-number-keying trap this lane hit before — re-keyed the ledger entry and cross-checked all 18 entries against the rule actually on each line, not just the count. Gate: 18 total, all ledgered. tests/ analyzer findings: 10 -> 0. The whole repo is now clean under AnalysisMode=All with warnings-as-errors. Verified: dotnet build tests -> Build succeeded (0 errors); dotnet test net8.0 -> Passed! Failed: 0, Passed: 2127, Total: 2127. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Regenerated by the SURFACE gate during a full run-ci; provenance line only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…d races
The widened LINT gate went red in CI with SIX MSB3030 ("Could not copy
SignalWire.dll because it was not found") and two CS0006 ("metadata file
ref/SignalWire.dll could not be found") — alongside ZERO analyzer findings. That
is a fault in this script, not in the code it lints, and it is the kind of red
that trains people to ignore a red.
Cause: every one of the other 18 projects ProjectReferences src/SignalWire, so
they all build the same shared output. Two things then went wrong:
* --no-incremental was passed to EACH project in turn, so building project N
wiped the shared library output that projects 1..N-1 had just been linked
against. Now the clean rebuild happens ONCE, on the shared library, and the
dependents build against that settled output. They are still fully analyzed:
--no-incremental controls output reuse, not whether analyzers run.
* MSBuild's default parallel node count rebuilt that shared reference
concurrently from several dependents, which clobbered it mid-copy. -m:1
makes the ordering deterministic. It costs wall time and buys a gate whose
failures mean something.
Caught by reading the KEPT gate log (.sw-tmp/ci-sched.*/15.log), not the
reporter's 40-line tail — the tail showed only "Build succeeded" for the last few
projects and no error at all. Worth remembering: run-ci's inline output is
truncated, and the per-gate log is where the cause lives.
Verified: bash scripts/run-lint.sh -> exit 0 on two consecutive runs,
"Build succeeded" x19, 0 errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Provenance-only: the generated_from SHA follows HEAD after the MSBuild serialization fix. No surface content changed (1 line, the stamp). Committed so SURFACE-FRESH compares a current stamp rather than reporting a dirty tree.
Same defect as the sibling fix in signalwire-java. `--strict` was declared
`action="store_true"` and documented in this script's usage header (line
22), but no gate ever passed it — all ten run-ci.sh scripts measure zero
`--strict` references, as does porting-sdk's _signatures_fresh.py. The
fail-loud branch (`if args.strict: return 1`) was therefore unreachable:
an untranslatable type silently dropped its whole symbol, the artifact was
written regardless, and the process exited 0.
Switched to argparse.BooleanOptionalAction with default=True, matching
rust and cpp. `--no-strict` remains as the explicit escape hatch.
dotnet's current tree translates clean, so this is a no-op on today's
artifact -- but it closes the hole for the next untranslatable type instead
of letting it rot the file silently.
Verified:
negative control emptied the dotnet alias table -> 1212 translation
failures, exit 1, artifact NOT written (sha256-checked)
positive control clean regen -> exit 0, artifact BYTE-IDENTICAL
unknown flag argparse already rejects it (exit 2), artifact untouched
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
The only change is the `generated_from` sha; the `native_names` payload is byte-identical. The artifact records the HEAD it was generated at, so every commit that does not touch the surface still leaves the stamp one commit behind, and the next run-ci regenerates it and dirties the tree. Committing the restamp so the tree is clean at HEAD. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…can now see
porting-sdk 8496c77 ("a class whose every method is a base-identical override
vanished") took the signature oracle from 7 of 18 skill modules to 18 of 18 — a
per-method skip was emptying such classes and dropping them entirely. dotnet
absorbed that as +46 signature drifts, all on signalwire.skills.*.
None of them is a missing implementation. All 11 builtin skills exist under
src/SignalWire/Skills/Builtin/ and every drifting member is declared there. Two
enumerator-side idioms were hiding behind the oracle's former blind spot:
1. INHERITED-OVERRIDE PROJECTION (26 missing-port). enumerate_surface.py has
carried SKILL_INHERITED_PROJECTIONS + _SKILLBASE_INHERITABLE for a while;
enumerate_signatures.py deliberately had no counterpart, on the premise that
"the SIGNATURE oracle (griffe) does NOT re-record inherited methods on a
subclass". 8496c77 falsified that premise: the oracle now records each
subclass's real declared overrides (DataSphereSkill genuinely declares
cleanup/get_hints/get_instance_key/get_parameter_schema/get_prompt_sections/
get_global_data/setup/register_tools in the reference source). C# reflection
reports only members whose declaring type IS the subclass, so an inherited
virtual is absent from the dump. Project the same per-class set the SURFACE
enumerator already projects, taking the signature from SkillBase.
This is the established fleet shape: ruby's +7 was a mirror gap between its
two enumerators; rust's was the analogous stale premise.
2. RECEIVER-IDIOM FOLD (20 param-count-mismatch). Python's
SkillBase.__init__(agent, params) stores both on the instance, so
setup(self) / register_tools(self) take nothing further. .NET's
Wire(agent, parameters) lifecycle passes them explicitly to
Setup(agent, parameters) / RegisterTools(agent). Same hook, same capability,
receiver-vs-explicit-argument binding — folded at the enumerator wherever the
shape occurs instead of class by class.
The fold makes 6 omission entries dead, so they are deleted (120 -> 114):
SkillBase.setup/register_tools, MCPGatewaySkill.setup/register_tools (both the
receiver idiom now folded), and DateTimeSkill.setup/register_tools — the latter
two were written ON the premise 8496c77 removed, i.e. they were excusing exactly
the blind spot the oracle fix closed.
Measured, --omissions on both sides:
drift 46 -> 0
port symbols 2473 -> 2509 (+36 added, 0 removed — exactly the projection table)
omissions 120 -> 114 (NO entries added)
excused 1358 -> 1362
Negative control: deleting the projected DataSphereSkill.get_instance_key from
the artifact re-reds the gate with 1 drift, so the projected members stay under
active comparison rather than becoming a blind spot.
KNOWN ORACLE RESIDUAL — left alone, not excused. 8496c77 did not fully close the
hole; five skill classes are enumerated but UNDER-enumerated, declaring members
in the reference source that the oracle omits. Their .NET overrides therefore
land as excused missing-reference additions (+10 port-only, which nets the +4
excused above). That is oracle work, not port work:
ApiNinjasTriviaSkill get_instance_key (skill.py:146), get_parameter_schema (:211)
PlayBackgroundFileSkill get_instance_key (skill.py:138), get_parameter_schema (:53)
SpiderSkill get_instance_key (skill.py:201), get_parameter_schema (:43),
cleanup (:669)
WeatherApiSkill get_parameter_schema (skill.py:49)
WikipediaSearchSkill get_parameter_schema (skill.py:44), get_hints (:212)
The last two were not previously identified as part of this residual.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…tract (#90)
#90 ("TLS unreachable / silently-plain-HTTP", 6 of 9 ports affected) put dotnet in
scope. Probed behaviourally rather than by grep: dotnet's TLS behaviour is CLEAN on
all four questions, but it had ZERO test coverage — nothing in tests/ referenced
CA_FILE, CaTrust, or a TLS handshake, so every property below could regress
silently. These tests carry the probe's assertions into the suite.
What the probe established, wire-level:
https:// base URL against a PLAIN TCP listener that answers a valid 200
WIRE first byte: 0x16 (TLS ClientHello)
WIRE first 8 bytes: 16 03 01 00 92 01 00 00
CLIENT OUTCOME: THREW SignalWireRestTransportError ... The SSL connection
could not be established
Negative control, http:// against the SAME listener
WIRE first 8 bytes: 47 45 54 20 2f 61 70 69 ("GET /api")
CLIENT OUTCOME: RETURNED 200-parsed payload {ok=True}
So the listener genuinely answers plaintext, and the https:// client genuinely
refuses to speak it — no silent downgrade.
Certificate verification, against a REAL TLS listener serving a self-signed leaf
issued by a throwaway CA:
SIGNALWIRE_REST_CA_FILE unset -> REJECTED (verification on by default)
SIGNALWIRE_REST_CA_FILE = issuing CA -> ACCEPTED (the fleet CA-var is wired)
SIGNALWIRE_REST_CA_FILE = OTHER CA -> REJECTED (validates, not blanket-accept)
The third case is the load-bearing one: a callback that returned true whenever the
env var happened to be set would pass the accepting case too. Only the wrong-CA
case separates a real trust root from a bundle-shaped opt-out.
Two grep hits were checked and are NOT findings:
* McpGatewaySkill.cs:490 DangerousAcceptAnyServerCertificateValidator sits inside
`if (!_verifySsl)`, and _verifySsl defaults TRUE (line 47 / line 173). That is
exact parity with the reference (skill.py:146 `verify_ssl = params.get(
"verify_ssl", True)`, forwarded as `verify=self.verify_ssl`) — a user opt-out,
not a default.
* CaTrust.Validate rescues ONLY RemoteCertificateChainErrors and re-validates
against the supplied root with CustomRootTrust; a name mismatch or missing cert
still rejects. It never blanket-returns true.
RELAY: Client.cs:288-291 forces the scheme to wss for anything that is not
explicitly ws/wss, and ApplyRelayCaTrust wires SIGNALWIRE_RELAY_CA_FILE through the
same audited CaTrust.Validate.
The 5 tests are RED-before / GREEN-after. Neutering BuildRestTransportHandler to
`handler.ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator` fails exactly the
two rejection tests on every framework (Failed: 2, Passed: 3 on net8.0/net9.0/
net10.0), including the wrong-CA case; restoring makes it 5/5 on all three. The
tests assert on captured wire bytes and real handshake outcomes, never on handler
configuration readback.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Provenance sha only — the native name list is byte-identical; neither the enumerator idiom fold nor the TLS tests changed public C# surface. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…axes
Re-drift against porting-sdk oracle 0e0f935, which the SURFACE-DIFF red traces to.
The reference made SkillBase.get_prompt_sections() a FINAL template method that
applies the skip_prompt guard and delegates to a PROTECTED _get_prompt_sections()
hook (signalwire-python core/skill_base.py:88-97). Every skill subclass now
overrides the protected hook, so the oracle records the public member on the BASE
ONLY — it dropped it from 11 skills on the surface axis.
NOT the cpp shape. cpp's defect was a stale HAND-KEPT py_methods list. dotnet has
no hand list in this path: all 12 C# skills genuinely declare
`public override List<Dictionary<string, object>> GetPromptSections()` under
src/SignalWire/Skills/Builtin/, so the member is PARSED FROM SOURCE and no table
edit could have removed it. .NET has no template-method split — each override
re-applies the guard itself via the SkillBase.SkipPrompt helper. Verified on all
12: behaviour is identical to the reference, so this is surface projection only,
with no wire bug. The C# override IS this port's spelling of the protected hook,
and its public name must not be emitted as subclass surface.
MIRROR GAP — the real finding, and why the fix is two-sided. enumerate_signatures
and enumerate_surface disagreed about the same 13 symbols: the signature
projection only ever ADDED members, so it kept emitting all 13 and they landed as
EXCUSED additions, while the surface axis went red on 11. Signature drift read 0
the whole time — the axis was not clean, it was absorbing. Both enumerators now
carry the same derived gate (_skillbase_final_{surface,signature}_members), and
each names the other so the pair stays in step.
THE GATE IS DERIVED, NOT HAND-LISTED: a _SKILLBASE_INHERITABLE member the oracle
records on SkillBase and on NO skill subclass is a final template method, and is
stripped from every subclass. A future reference move of this shape self-corrects
on the next regen.
WHY "base-only" AND NOT A PER-CLASS INTERSECTION (measured, not theoretical). The
plain intersection cpp uses removes 34 symbols here, not 13 — it also deletes
setup / register_tools / get_instance_key / get_parameter_schema / get_hints /
cleanup from ApiNinjasTrivia / PlayBackgroundFile / Spider / WeatherApi /
WikipediaSearch. Those are the KNOWN oracle under-enumeration residual (2f65211):
the members ARE declared in the reference source — spider/skill.py:43,201,206,241,
656,669; api_ninjas_trivia/skill.py:127,137,146,211; weather_api/skill.py:49,113,
123 — and the griffe oracle simply misses them. Deleting real .NET implementations
because an oracle bug hides their twin would be hiding port surface, so the gate
tests "the reference exposes it on the base and on no subclass" instead. Only
get_prompt_sections meets that test today. CustomSkillsSkill (a .NET-only skill
with no reference module) is likewise untouched.
DEAD ENTRIES DELETED (required, not optional):
PORT_ADDITIONS.md ClaudeSkillsSkill.get_prompt_sections
InfoGathererSkill.get_prompt_sections
Both rationales read "Python ships the same overrides" — no longer true (Python
ships them on the protected hook) and the port no longer emits the public name, so
they excused nothing. The gate itself flagged them as DEAD once the fix landed.
Measured, --omissions + --surface-omissions + --surface-additions on both sides:
surface drift 11 -> 0
signature drift 0 -> 0
port symbols surface 2575 -> 2562, signature 2509 -> 2496
(-13 on each axis, +0 — exactly the 13 get_prompt_sections,
identical sets; the axes now agree symbol for symbol)
excused (sig) 1372 -> 1359 FELL by the 13; absorbed nothing
excused (surf) 360 -> 347 FELL by 13; absorbed nothing. 11 were the
newly-folded symbols (never ledgered — they were the RED),
2 were the deleted dead entries.
PORT_ADDITIONS line count -2 (two DELETIONS; zero insertions)
PORT_OMISSIONS / PORT_SIGNATURE_OMISSIONS unchanged
Negative controls, three arms:
- The gate is oracle-driven, not hardcoded: re-adding get_prompt_sections to
ONE subclass in a temp oracle collapses the final-member set to empty (the
member is no longer base-only) with no edit to any table.
- Fail-safe: an oracle recording nothing for SkillBase yields an empty set, so
the gate disables rather than stripping every lifecycle member from every
skill.
- End-to-end: appending JokeSkill.get_prompt_sections to the committed
port_surface.json re-reds SURFACE-DIFF with exactly 1 finding. Restored; green.
RELAY, and a correction to the expected shape: porting-sdk be7a34f added
calling.conference.{params,result}.json as permissive placeholders (type: object,
additionalProperties: true, x-permissive: true, no properties) after
mod_infrastructure 9755ef7 registered a second protocol method. dotnet's RELAY
generator already filters these — build_outputs skips any node failing
GR.is_object_schema — so the regen is a genuine NO-OP here (0 new types, empty
diff), not the +2 aliases measured on typescript. GEN-FRESH-RELAY --check exit 0.
New server surface, not drift.
port_surface_native.json: regenerated, PROVENANCE-ONLY (native_names delta 0
added / 0 removed). The enumerator's --check strips generated_from, so the stamp
is not gate-relevant; left at HEAD rather than committing regen residue.
No C# changed (0 .cs files in the diff), so the .NET suite is unaffected;
ruff check + ruff format --check are clean on both edited enumerators.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…rrent SDK
Measured before this change: 16 of 57 examples/*.cs failed to compile against
the built SDK (39 unique CS errors). None of them were caught by CI, because
only 4 of the 57 had a .csproj — the other 53 were compiled by nothing.
Root causes, by group:
* SWMLService -> SignalWire.SWML.Service (5 files). The class was renamed and
its ctor became an options object; the examples still used the old name and
named ctor params. `AddAnswerVerb()` does not exist on Service at all (it is
an AgentBase post-answer-verb concept, and the reference SWMLService has no
add_answer_verb either) — replaced with AddVerb("answer", ...), which is
what the schema actually declares.
* DynamicSwmlService called `service.OnRequest(lambda)`. OnRequest is a
virtual OVERRIDE hook in both this port and the reference, never a callback
registration — so that call was never valid API. Rewritten to the idiomatic
form: a Service subclass overriding OnSwmlRequest.
* Contexts (ContextsDemo, GatherInfoDemo). `AddContext(name, dict)` does not
exist; AddContext(name) returns a fluent Context and AddStep(name) returns a
fluent Step. Beyond the compile error these examples used the WRONG KEYS —
step "prompt"/"criteria" (real: "text"/"step_criteria") and gather question
"field"/"text" (real: "key"/"question", matching the reference's
GatherQuestion(key, question)). Those keys would have been silently dropped
or thrown KeyNotFoundException at runtime.
* RestDemo predated the typed REST surface: ListAsync() now returns typed
responses (PhoneNumberListResponse etc.), not Dictionary<string,object?>.
Rewritten against the typed API, which is also the better demo — fields are
compiler-checked properties instead of string lookups.
* TapExample passed Tap() a dictionary; the API takes typed named args. It
also used direction "listen", which is not in the valid set
{speak, hear, both} and would have thrown at runtime — corrected to "hear",
and the trailing prose that documented "listen" corrected with it.
* CallFlowAndActionsDemo called EnableDebugEvents("all"). The parameter is an
int verbosity level (1 = high-level, 2+ = adds high-volume), per the
reference and its API docs — "all" is not a value the API accepts.
* FunctionResult.SendSms(to:, from:) -> (toNumber:, fromNumber:) (2 files).
* DataMap.Foreach -> ForEach; DataMap.Params takes Dictionary<string,object>.
* InfoGatherer examples were missing `using SignalWire.Agent;` for AgentOptions.
* ReceptionistAgent's route/greeting go through its options dictionary.
Also brings the examples up to the repo-wide analyzer bar, which they had never
been held to (see the companion commit that gives them projects): culture-
explicit ToLowerInvariant/CultureInfo.InvariantCulture, cached
JsonSerializerOptions, `using`/`await using` on the disposable clients, and the
HttpClient.GetAsync(Uri) overload. The RELAY Client is IAsyncDisposable, so the
quickstarts use `await using` — which is better teaching than the bare `var`
they had. README.md's included quickstart blocks are updated to match (the
README-INCLUDE gate asserts byte-identity).
.editorconfig gains examples-scoped grants ONLY for rules whose remedy would
damage the example as teaching material — CA2007 (Microsoft's own guidance
scopes it to library code, not application entry points), CA1031 (broad catch IS
the demonstrated behavior in a multi-step demo), CA5394 (picking a random joke
is not a security context), CA1861 (one-shot literal in a top-level program) and
CA1308. Every finding with a clean code fix was fixed in code instead.
Not changed, reported instead: the port's ReceptionistAgent drops the
reference's `voice` parameter (python feeds it to add_language(...)); the
example now calls AddLanguage explicitly. That is a real parity gap needing a
ruling, not an example fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
`HttpListener.Start()` DISPOSES the listener when it fails. Service.RunHttp's
wildcard-bind fallback reused that same instance — `listener.Prefixes.Clear()`
— so the retry threw ObjectDisposedException instead of retrying, and the
`InvalidOperationException` with the actionable "failed to bind" message on the
non-wildcard path was equally unreachable.
Net effect: EVERY bind failure surfaced as an opaque
Unhandled exception. System.ObjectDisposedException: Cannot access a
disposed object. Object name: 'System.Net.HttpListener'.
at System.Net.HttpListener.get_Prefixes()
at SignalWire.SWML.Service.RunHttp(...)
with no indication of the real cause. Found by giving the shipped examples a run
target (task #204): with an unrelated process holding :3000, all ~30 server
examples died with that stack. Isolated repro, HttpListener alone:
Start() threw HttpListenerException: code=48 msg=Address already in use
Prefixes.Clear() threw ObjectDisposedException -> the failed Start()
DISPOSED the listener
The bind logic moves into a BindListener helper that builds a FRESH listener for
the localhost fallback and closes the failed one, so the fallback actually
happens and a genuinely unbindable port reports why (which process/port, and the
Linux privileged-bind hint) instead of a disposed-object stack.
Covered by tests/SwmlServiceBindFailureTests.cs. Negative control: reverting the
helper to the old reuse-the-disposed-listener form turns
OccupiedPortReportsWhyInsteadOfObjectDisposed RED on all three TFMs; restoring
it turns it GREEN.
No public surface change — BindListener is private, and Run()/RunForTest keep
their signatures.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…relay examples
THE BLIND SPOT. 71 examples are tracked across three roots — examples/ (57),
rest/examples/ (11) and relay/examples/ (3) — and only FOUR of them had a
.csproj. The other 67 were in no solution, invisible to run-lint.sh's project
enumeration, and compiled by NOTHING. Nothing in CI would ever notice a shipped
example that stopped compiling, which is how 16 examples/ files and all 14
rest+relay files came to ship broken.
Closing it:
* scripts/generate_example_projects.py writes one <Stem>.csproj next to every
tracked example plus a per-root Directory.Build.props, and `--check` asserts
the on-disk set matches exactly (no example without a project, no orphan
project, no drift from the template).
* run-ci.sh gains EXAMPLES-PROJECTS (cheap, non-deferred) running that
`--check`. It does not compile anything itself — it does not need to.
scripts/_env.sh:dotnet_all_projects `find`s every *.csproj on disk, so the
moment an example HAS a project the existing LINT gate builds it under the
repo-wide analyzer bar (AnalysisMode=All, TreatWarningsAsErrors). The
project-coverage check is the only missing link, and it is what this adds.
* EXAMPLES-RUN stops self-skipping. porting-sdk's examples_run.py moves dotnet
from "compiled" (SKIPPED-WITH-NOTE) to "compiled-runnable" with
`dotnet run --project <root>/<Stem>.csproj`, the same shape java uses for
gradle runExample. The run-ci gate keeps MOCK_RELAY_STRICT=1, so a
wrong-wire example now fails loud against the strict mock. res=msbuild so it
serialises with the other MSBuild-driving gates.
Directory.Build.props per root is load-bearing twice over: it sets
BaseIntermediateOutputPath BEFORE Microsoft.Common.props (setting it in a
.csproj body raises MSB3539 and splits restore from build, leaving 71 projects
racing one shared project.assets.json), and it re-imports the repo-root
Directory.Build.props so the examples keep the analyzer bar rather than silently
dropping off it.
The 14 rest/relay examples are fixed here too, since they became visible only
once they had projects. Same typed-REST migration as examples/RestDemo.cs: the
generated resources return typed DTOs, so `resp.GetValueOrDefault("id")` becomes
`resp.Id` and `data as List<object>` becomes the typed `Data` list. Specific
finds beyond the mechanical change, each checked against the SDK or the vendored
spec rather than guessed:
* RestCallingIvrAndAi called client.Calling.Collect/Ai/Transcribe/Tap/Stream —
none of which exist. The real verbs are CollectAsync/LiveTranscribeAsync/
TapAsync/StreamAsync with TYPED parameters, not one opaque config dict. Its
wire shapes were wrong as well, corrected against rest-apis/calling:
live_transcribe `direction` is a LIST (["local-caller","remote-caller"]),
not "both"; stream `track` is `both_tracks`, not "both"; tap takes `tap` AND
`device` as two separate required params. There is also no REST verb that
STARTS an AI session (the ai SWML verb does that) — the example now
demonstrates AiMessageAsync on a running session and says so.
* relay/examples/RelayDialAndPlay called WaitAsync(timeoutSeconds:); the
parameter is `timeout` (seconds, double?).
* RestPhoneNumberManagement read a `carrier_name` field that does not exist —
carrier detail is the nested `carrier` object (Lec / Linetype).
* A SIP profile has no id; it is keyed by Domain.
* AvailablePhoneNumber has Number, not E164; fabric resources use DisplayName;
Queue uses FriendlyName.
* RecordingListResponse.Data is List<object?> (the spec leaves the item shape
open), so those items are read via the JsonElement API and the example says
why.
.editorconfig's examples grants are widened from `examples/**.cs` to all three
roots — scoped to one root they silently missed the other two.
examples/README.md drops the copy-into-a-scaffold dance for
`dotnet run --project examples/<Name>.csproj`, which now just works.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
RestClient hardcoded `https://{space}`, with no loopback detection. The
reference has had it since the typed-REST work — signalwire/rest/_base.py:
scheme = "http" if _is_loopback_host(host) else "https"
and php mirrors it explicitly citing that function. dotnet was the outlier.
Consequence: a shipped REST example pointed at a local mock with
SIGNALWIRE_SPACE=127.0.0.1:<port> — exactly how the EXAMPLES-RUN harness injects
it — could never reach the mock. Every request died with
GET https://127.0.0.1:58247/api/... failed:
The SSL connection could not be established
which is what turned 28 of the 71 examples red the first time the run gate
actually ran them.
BuildBaseUrl now honors an explicit scheme verbatim (trimming a trailing slash),
picks http:// for a bare loopback host (127.0.0.1 / localhost / ::1, with or
without a port), and https:// for everything else. A real space
(<name>.signalwire.com) is never loopback, so production behavior is unchanged;
"localhost.example.com" is likewise not loopback and stays https (covered).
No public surface change — IsLoopbackHost/BuildBaseUrl are private, and BaseUrl
keeps its type and meaning.
Covered by tests/RestClientLoopbackSchemeTests.cs (10 cases across the three
branches: loopback, real space, explicit scheme).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
The bind-with-fallback in Service.RunHttp existed in a SECOND, independent copy in AgentServer.Run, with the identical defect: a failed HttpListener.Start() disposes the listener, so the fallback's `listener.Prefixes.Clear()` threw ObjectDisposedException and the localhost retry never ran. Fixing only the SWML copy left every multi-agent example (MultiAgentServer, LlmParamsDemo, WebSearchMultiInstanceDemo, the Datasphere multi-instance demos) still dying with the same stack, just via AgentServer.Run:308 instead of Service.RunHttp. Swept src/ for other copies of the pattern; these two were the only ones. Same fix as the SWML side: a private BindListener that closes the failed listener and builds a FRESH one for the localhost fallback, and reports an actionable InvalidOperationException when nothing can bind. No public surface change — BindListener is private and Run keeps its signature. Covered by the new AgentServerOccupiedPortReportsWhyInsteadOfObjectDisposed case in tests/SwmlServiceBindFailureTests.cs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…ured counts The comment said 53 of 57, from before the generator was widened past examples/ to the other two roots the shared gate globs. The measured figure is 67 of 71 tracked examples with no project (16 broken under examples/, all 14 broken under rest/+relay/examples/). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…s staled
`generate_swaig_payloads.py --check` was red with 6 stale files. Two porting-sdk
re-vendors changed generator inputs with no fan-out step:
4336b98 re-vendor all eleven server specs -> swaig-specs/post-prompt.yaml
99fd429 swaig re-vendor at mod_openai -> swaig-specs/swaig-response.yaml
Every line of this diff traces to one of them.
From 99fd429 (`SwaigAction` gained types the old spec left untyped):
HoldAction.Timeout long? -> double? (integer -> ["number","string"])
TransferAction.Dest object? -> string? ((none) -> string)
PlaybackBgAction.File object? -> string? ((none) -> string)
ContextSwitchAction.SystemPrompt / .UserPrompt
object? -> string? ((none) -> string)
`ContextSwitchAction`'s properties also reorder, because the spec's own key order
changed in 99fd429 (system_prompt-first -> alphabetical). Not a generator change.
`timeout`'s `["number","string"]` reaches `double?` through `_type_schema_type`
taking the first non-null member of a type LIST, then `_TYPE_SCALAR_CS["number"]`.
Source-incompatible in principle, but these DTOs are method-less with no
hand-written consumer: the only other `HoldAction`/`ContextSwitchAction` spellings
in the tree are the unrelated Calling/Fabric/SwmlVerbs generated classes.
From 4336b98, two committed types were genuinely WRONG and are now corrected
(the same pair the perl/php regen caught):
PostPromptSwaigLogEntry.mcp_response Dictionary<string,object?>? -> string?
PostPromptSwaigLogEntry.mcp_error string? -> bool?
The spec types them `string` and `boolean` respectively; `mcp_response` is
documented as the MCP tool's raw result TEXT, explicitly "not parsed JSON", so the
Dictionary would have thrown `JsonException` on a real wire value.
Also from 4336b98, three flat accessors drop from `PostPromptSystemLogEntry`:
`context`, `step`, `step_index`. They are now nested inside `metadata.properties`
where `tl_stamp_location` actually stamps them, and were never on the wire at the
entry level.
Unlike the other ports, `PostPromptSystemLogEntry.action` stays `string?` here
even though 4336b98 gave it a closed 27-value enum: the .NET `_TYPE_SCALAR_CS`
mapping has no `enum` guard, so an enum-bearing string keeps its scalar type
rather than falling through to the open form.
Ripple: `port_signatures.json` only, -27 lines, zero additions -- the three
dropped accessors. The `mcp_error`/`mcp_response` retypes are invisible there
because the oracle records those accessors as `returns: "any"`. No surface
artifact needed changing; `port_surface_native.json` never carried the three
names, and `port_surface.json` carries `context` only via five other symbols.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016i9TibArqEmDszG7VYEGYg
…ing cross-file $ref members
The `Doc audit` job (run 30907242119) failed on 2 unresolved symbols with two
different causes, and neither was a doc bug.
1. `ForEach` — a CHECK bug, not a phantom API.
`DataMap.ForEach` (src/SignalWire/DataMap/DataMap.cs:207) is real public
surface, present in `port_surface.json` (as the canonical `foreach`, folded by
METHOD_RENAMES) AND in `port_surface_native.json` (verbatim `ForEach`).
`.github/workflows/doc-audit.yml` simply never passed `--native-names`, so
`audit_docs.load_native_names()` returned the empty set and every member the
enumerator translates became unresolvable — the gate blaming the docs for the
enumerator's own snake_case/Async-stripping translation.
`scripts/run-ci.sh` was ALWAYS correct here (porting-sdk
`scripts/suites/_doc_audit.py:142` auto-detects the sidecar for any port that
ships one), which is exactly why local run-ci was green while the workflow was
red. Measured: without the flag, 2 blocking + 258 report-only; with it,
1 blocking + 0 report-only. Same class as the java case that
`load_native_names()`'s own docstring records for 2026-07-25.
2. `ToLowerInvariant` — a genuine BCL external, remedy (c).
`ToLower`, `ToUpper` and `ToUpperInvariant` were all already ledgered; only the
culture-invariant lower twin was missing. Swept the doc perimeter for the whole
BCL string family: `ToLowerInvariant` is the sole gap (Trim/ToUpperInvariant
already resolve), so this is one entry, not a class of them.
Two further enumerator defects found while verifying, both of the
"fails SUCCESSFULLY" shape — a wrong parity artifact emitted at exit 0:
3. Cross-file `$ref` silently dropped surface members. porting-sdk re-vendor
99fd429 rewrote `PostPromptSwaigLogEntry.post_data` / `.post_response` /
`.delayed_post_response` from same-file `#/components/schemas/...` to cross-file
`swaig-{request,response}.yaml#/...`. `_local_ref()` tested only
`startswith("#/")`, so all three stopped counting as composition members while
the oracle still records them. The surface silently shrank and SURFACE-DIFF
accused the PORT of omissions it never had. `_local_ref` is now `_class_ref`,
accepting cross-file refs against a registered `CROSS_FILE_SPEC_FILES` set and
RAISING on an unregistered target — the doctrine porting-sdk's
`generate_python_rest_types.py` already states ("a cross-file ref into a file
that is not registered is an error, not a dict fallback"). Surface +2 members;
SURFACE-DIFF 4 missing -> 2.
The new fail-loud immediately found a third legitimate ref form:
schema.json's `$id`-rooted bare `"SWMLObject.json"`, registered as
SELF_ID_ROOT_REFS (resolves to the document root, not a model class).
4. Two swallowed exceptions made fatal.
* The `generate_rest.py` load failure warned and continued. On an interpreter
without PyYAML that emitted 1291 methods instead of 1578 — 287 silently gone,
a 460-line phantom diff, exit 0. Its own comment already documented that this
had cost a full CI investigation; warning was not enough. Now fatal, with
`SW_ENUM_ALLOW_NO_ORACLE=1` as a deliberate opt-in to the degraded mode.
* `build_native_names`'s broad `except Exception: continue` emitted a SHORT
sidecar, which is precisely the failure that would make DOC-AUDIT report a
real member as a phantom. Now fatal.
The 2 members the surface gains are the whole gain; DataMap was never absent.
Verification
audit_docs.py --native-names ... exit 0
✓ docs/examples reference only known symbols (1133 resolved / 2007 total)
run-ci DOC-TRUTH:DOC-AUDIT PASS
diff_port_surface.py 4 missing -> 2
enumerate_surface.py (regen) byte-stable across 3 runs
Negative controls (all restored byte-identical):
PyYAML absent -> exit 1, artifact UNWRITTEN (was: exit 0, 460-line short)
unregister a cross-file spec -> exit 1 naming the ref (was: 2 members silently dropped)
remove ForEach from sidecar -> doc gate exit 1, names AdvancedDataMapDemo.cs:67
Still red on this branch, all pre-existing and unrelated (verified by stashing):
LEDGER (6 wave-A .editorconfig severities), SURFACE (DRIFT/SURFACE-DIFF: C# has no
`SwaigAction`/`SwaigResponse` container type — a real port gap from the same
re-vendor, left for an owner ruling), DOC-TRUTH (README-INCLUDE, 3 fixture drifts),
LINT (S603/S607 in generate_example_projects.py, red at HEAD too).
FLEET FINDING (not fixed here — other ports are other lanes): 5 of 10 ports omit
`--native-names` in their doc-audit workflow — go, java, perl, ruby, typescript.
java is notable, being the port whose sidecar bug the loader docstring documents.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016i9TibArqEmDszG7VYEGYg
…updating
`AnalysisLevel=latest` + `AnalysisMode=All` + `TreatWarningsAsErrors=true`, with NO
global.json anywhere, is a SELF-UPDATING RULE SET. `latest` means "every rule this
SDK knows about", `All` enables them at default severity, warnings-as-errors makes
each one a build failure — so a new .NET SDK, including a PATCH bump on a CI runner
image, can switch on new CA rules that instantly break the build. No package release
and no commit of ours is involved. The LINT gate's verdict depended on WHICH SDK the
runner happened to install rather than on our source, and no local run could
reproduce the CI red.
Directory.Build.props:28 <AnalysisLevel>latest</AnalysisLevel> -> 10.0
(new) global.json sdk 10.0.100, rollForward latestPatch, allowPrerelease false
requirements-dev.txt:13 ruff>=0.15 -> ruff==0.15.21 (audit missed this one)
Two halves, because either alone leaks: the numbered AnalysisLevel fixes the rule SET
(a newer SDK still builds, but does not silently enable rules we have not burned
down), and global.json fixes the toolchain that supplies the analyzers, the code-style
rules and the compiler. rollForward=latestPatch keeps the 10.0.1xx band's patch
updates working — patch releases do not add analyzer rules — so only a feature-band or
major bump is deliberate. The library still multi-targets net8.0/net9.0/net10.0;
TargetFrameworks is what we SHIP for, global.json only what BUILDS it.
ruff was `>=0.15` — an open ceiling, the same defect one layer over. CI installs from
requirements-dev.txt (`pip install -r`, not a bare `pip install ruff`), so pinning the
manifest is the whole fix; no workflow edit needed.
ONE new violation surfaced, and it came from the ruff pin, not the analyzer pin:
moving ruff 0.14.2 -> 0.15.21 fired S603 (subprocess call) + S607 (partial executable
path) on scripts/generate_example_projects.py:87-88. Not suppressed to go green — the
site is `git -C <this repo> ls-files <example roots>/*.cs`, and asking GIT rather than
walking the filesystem is the POINT: it is what keeps untracked scratch .cs files out
of the generated project list, which no os.walk/Path.glob can replicate. ruff.toml
already carried a documented per-file S603/S607 grant for three structurally IDENTICAL
siblings (enumerate_surface.py's `git -C <repo> rev-parse HEAD`, generate_rest_tests.py's
`bash <repo>/scripts/rest-test-plan.sh`) — fixed arg vector, this repo's own path,
shell=False. This file was simply missing from that list; it is now added to the same
per-file grant with its own rationale line, so a NEW subprocess call anywhere else
still reds the gate.
Verified, including the suite (a lint fix can BE the bug — CA2000 `using` once took
this repo from 2127 passing to 114 ObjectDisposedException, so a green linter is not
evidence):
scripts/run-lint.sh exit 0 — ruff "All checks passed!", then dotnet build across
every project/TFM with analyzers on: 0 Error(s)
scripts/run-tests.sh exit 0 — Passed! Failed: 0, Passed: 2145 on EACH of net8.0,
net9.0, net10.0 (6435 total, 0 failures)
So AnalysisLevel 10.0 finds exactly what `latest` found on this SDK (0), and nothing
regressed.
Not from this change: run-ci's LEDGER gate reds on SUPPRESSION-LEDGER with 6 wave-A
REPORT-ONLY `.editorconfig` severity-none entries (lines 204-255). .editorconfig is
untouched here — those came in with the examples work (90c4612). The
port_surface_native.json provenance-sha restamp a CI run produced is likewise not
staged in this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016i9TibArqEmDszG7VYEGYg
Two real Windows defects from the multi-OS nightly (run 30908589549), each
invisible to the linux-only PR tier.
1. POM SERIALIZATION EMITTED CRLF ON WINDOWS — a wire divergence.
Failed ToJson_ExactShape: Expected "[\n {\n …" Actual "[\r\n {\r\n …"
Failed ToYaml_ExactShape: Expected "- title: A\n" Actual "- title: A\r\n"
POM JSON/YAML is wire output, and the reference is LF on EVERY platform: Python's
json.dumps(indent=2) and yaml.dump(default_flow_style=False) hardcode "\n" and never
consult the OS (verified by running both). But System.Text.Json's indent newline —
before .NET 9's JsonSerializerOptions.NewLine, which we cannot use while net8.0 is in
the matrix — and YamlDotNet's serializer both default to Environment.NewLine. So the
BYTES this SDK sent differed by platform. The tests were right; the code was wrong.
Normalized in one private helper used by both serializers, so the fix is a single
code path across net8.0/net9.0/net10.0. Stripping bare CR is safe here: a CR inside a
string VALUE is escaped by the serializers (\r in JSON, quoted in YAML), so CR can
only ever be a line ending in this output. Swept the rest of src/ for
Environment.NewLine / AppendLine in emitting code — POM was the only site.
2. THE MOCK HARNESS DID NOT RECOGNISE A WINDOWS LOST BIND.
Failed SelfSpawn_RecoversWhenTheMockLosesTheBind_InsteadOfServingADeadEndpoint
harness must classify a lost bind as retryable; got stderr=
[Errno 10048] … [winerror 10048] only one usage of each socket address
(protocol/network address/port) is normally permitted
IsAddressInUse matched "address already in use", errno 48 (macOS), errno 98 (Linux)
and EADDRINUSE. Winsock's message contains NONE of those, so on Windows it returned
false — and that is not a cosmetic misclassification: EnsureServer treats
not-address-in-use as a FATAL startup error instead of retrying on a fresh port, so
one benign port collision fails every mock-backed test in the run with
connection-refused. Added the Winsock forms (10048 / WSAEADDRINUSE / the prose).
TESTS. Both fixes get a platform-independent guard, so neither depends on the suite
happening to run on Windows:
* Serialization_IsLfOnly_OnEveryPlatform asserts no CR in ToJson/ToYaml, plus that
newlines are present at all (a no-CR assertion on single-line output would pass
vacuously).
* The existing AddressInUseDetector theory gains the VERBATIM Windows message as a
data row. Negative-controlled: on the pre-fix detector exactly that row fails
(1 failed / 6 passed); with the fix, 40/40 in the POM + detector filter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016i9TibArqEmDszG7VYEGYg
First complete `==> CI` verdict for this repo on the branch. The prior lane's run-ci was still inside FMT when its turn ended, so only partial gate results had ever been reported. Full run at e7674b2: ==> CI FAIL (gates: LEDGER ROOT-HYGIENE SURFACE DOC-TRUTH ) The wrapper exited **0** while the log said FAIL — the run-ci exit code is not the verdict, the `==>` line is. Four-for-four on that trap today across lanes. CONFIRMED FIXED by e7674b2: the four gates that previously traced to the single CA1307 build error at PromptObjectModel.cs:361 are gone. SIGNATURES-FRESH now PASSES, which is the load-bearing one — its enumerator had been CRASHING (`RuntimeError: SignatureDump failed (exit 1)`) on that compile error rather than reporting drift, so one build break was masquerading as four independent failures. LINT and TEST also PASS. Three of the four remaining reds were bookkeeping, and are fixed here. 1. DOC-TRUTH / README-INCLUDE (3 sites) — fixture drift from 175ad01. That commit reformatted the three quickstart fixtures from column-aligned named args / initializers to single-space (`name: "get_time"` -> `name: "get_time"`, `Project =` -> `Project =`) but did not update the README blocks the gate byte-compares them against. The fixtures are what LINT compiles and FMT formats, so the fixture wins; the README blocks are realigned to match. Gate: 3 offending sites -> clean (3 verified). 2. ROOT-HYGIENE — global.json, added by a97e930, not on the allowlist. Allowlisted, on proven location-mandate grounds rather than convenience. The .NET SDK discovers global.json ONLY by walking UP from the build directory, so at any other path the analyzer-stability pin silently applies to nothing. Negative control with an unsatisfiable pin (99.0.100, rollForward=disable): under eng/, `dotnet --version` in a sibling dir IGNORED it and printed 10.0.108; at the root of the walk, the same command failed to resolve an SDK. Same grounds as the existing ruff.toml entry. 3. LEDGER / SUPPRESSION-LEDGER (6 "NEW" disables) — a stale line key plus five genuinely unrecorded grants, both from 175ad01. The ledger is keyed `<relpath>:<line>`. 175ad01 added 51 lines to .editorconfig: it introduced five examples-scoped grants (CA2007, CA1031, CA5394, CA1861, CA1308) without ledgering them, AND shifted the already- ledgered CA1303 from :199 to :204, so that one reported as new too. Each rationale already existed in .editorconfig and is carried over verbatim — this records reasoning that was written but never filed, it does not excuse anything new. All six stay ON for src/. Header count corrected 18 -> 23, which the gate now confirms ("23 total, all ledgered or absent"). The five carried entries are marked pending owner confirmation: the ledger format records an approver, and I will not sign one on the owner's behalf. STILL RED, left for an owner ruling — SURFACE (DRIFT + SURFACE-DIFF), a real port gap, not bookkeeping: ✗ 2 Python symbol(s) missing from port: signalwire.core.swaig_actions_generated.SwaigAction signalwire.core.swaig_actions_generated.SwaigResponse ✗ 5 signature drift(s): SwaigAction.{context_switch,hold,playback_bg,transfer}, SwaigResponse.action — all missing-port scripts/generate_swaig_payloads.py:181 reads `spec[...]["SwaigAction"]["properties"]` and emits one class per PROPERTY (ContextSwitchAction, HoldAction, PlaybackBgAction ... all present), but never emits the SwaigAction / SwaigResponse CONTAINER types the oracle records. Implementing them is generator work with a wire contract to settle, and creating a PORT_OMISSIONS entry is not an agent decision. CORRECTION — 2e78d71's FLEET FINDING is FALSE, and this commit says so beside the flag in .github/workflows/doc-audit.yml, where anyone grepping for `--native-names` will land (that grep is exactly what produced the bad claim). That message asserted go, java, perl, ruby and typescript "omit --native-names" and would mis-blame their docs. Re-measured by RUNNING audit_docs.py with each port's own workflow arguments: all five exit 0, 0 blocking and 0 report-only. The flag is not a fleet requirement; it is the remedy for one of two shapes, decided by WHICH SURFACE FILE the workflow feeds the audit: two-surface (canonical --surface + native --native-names): dotnet, php, rust, cpp, python one native surface (--surface is already native): go, java, perl, ruby, typescript The dotnet fix in 2e78d71 is correct and stands — dotnet genuinely is a two-surface port whose workflow omitted the flag. java is the case that makes "does it ship a sidecar?" the wrong test: it DOES ship port_surface_native.json and DOES omit the flag, yet is correct, because it passes that native file directly as `--surface`. Go likewise passes a natively spelled port_surface_go.json. Published history is left alone; the correction is a forward note. Verification run-ci.sh (full, 25 pr-tier gates) ==> CI FAIL (LEDGER ROOT-HYGIENE SURFACE DOC-TRUTH) suites/doc_truth.py exit 0 — all 8 rules PASS, README-INCLUDE 3/3 clean root_hygiene.py exit 0 — clean suites/ledger.py exit 0 — both rules PASS, 23 ledgered ruff check scripts/ tests/ exit 0 — All checks passed (2e78d71's message claimed LINT was red on S603/S607 in generate_example_projects.py; that is stale — ruff.toml:72 already grants both, and the full run's LINT gate PASSED.) audit_docs.py, each port's own workflow args: java (--surface port_surface_native.json) exit 0 6056 resolved / 7293 go (--surface port_surface_go.json) exit 0 2992 resolved / 4605 perl (--surface port_surface.json) exit 0 2204 resolved / 2717 ruby (--surface port_surface.json) exit 0 1951 resolved / 2619 ts (--surface docs_audit_surface.json) exit 0 2445 resolved / 2718 Also corrected from the brief I was given: the 6 LEDGER entries are from 832e15c (1) and 175ad01 (5), not 90c4612 — confirmed by git blame per line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016i9TibArqEmDszG7VYEGYg
Provenance line only (generated_from e7674b2 -> b452965); the native_names list is byte-identical. The SURFACE gate regenerates this artifact on every run, so leaving it uncommitted means every subsequent run starts with a dirty tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016i9TibArqEmDszG7VYEGYg
…ecords `==> CI FAIL (gates: SURFACE)` at d850268 — 5 DRIFT + 2 SURFACE-DIFF, one gap: ✗ 5 signature drift(s) gen-payload.SwaigAction.{context_switch,hold,playback_bg,transfer}: missing-port gen-payload.SwaigResponse.action: missing-port ✗ 2 Python symbol(s) missing from port signalwire.core.swaig_actions_generated.{SwaigAction,SwaigResponse} generate_swaig_payloads.py read `SwaigAction["properties"]` and emitted one class per PROPERTY (the 4 `<Verb>Action` value objects) but never the two CONTAINER schemas swaig-response.yaml declares in its own right. Same gap python (porting-sdk 4ddda70), typescript (ca0dd9f) and go (41a012c) already closed; in go its absence did not even COMPILE (`undefined: SwaigResponse`). Envelope field types are built from the SAME per-verb schemas the `<Verb>Action` lift consumes, with each lifted object branch swapped for a `$ref` to its class — so the envelope and its members cannot drift, exactly as the three landed ports do it. That is what makes the four fields class-typed and therefore accessors: `context_switch`/`hold`/`playback_bg`/`transfer` are the only action verbs whose value lifts to a class, and they are precisely the four the oracle records. `SwaigResponse.action` is `oneOf: [$ref SwaigAction, array of $ref SwaigAction]`. C# has no sum type, so the property takes the `SwaigAction` arm — the same choice go made (goType widens to `any` while the canonical tag stays precise). The reference records `union<class:…SwaigAction,list<class:…SwaigAction>>`; the port's recorded accessor return is `any`, which `types_compatible` treats as compatible with any reference return, so the arm choice is parity-neutral and the accessor stays class-typed. Nothing narrower is derivable from the spec. enumerate_signatures.py: `_SIG_ACCESSOR_MODULES` listed swaig_actions_generated as wholly ABSENT from the signature oracle. That was true when written and stopped being true at 4ddda70. It is now a SPLIT module — the 4 value classes stay method-less, the 2 envelopes carry accessors — so a blanket per-module rule is wrong in both directions. New `_ORACLE_GATED_SIG_MODULES` intersects the emitted accessor set with `_oracle_class_members(module, class)`, the same oracle-gating the relay Event / AI-Chat DTO / SWMLService allowlists already use. Nothing about the split is hand-listed; the oracle arbitrates. The envelopes are emitted wire-key-verbatim (NOT pascal_props) because unlike the value classes they ARE accessor-bearing and the oracle records their field names wire-key-verbatim; PascalCasing them would drift SURFACE-DIFF. php and perl skip these two with the rationale "not part of the cross-port surface oracle" (generate_swaig_payloads.py:23-24 in both). That was accurate pre-4ddda70 and is now false — python_surface.json records both classes and python_signatures.json records their 5 accessors. It does not transfer to dotnet, and those two ports have the same live gap. Regenerated: 20 -> 22 SWAIG-payload classes, matching the oracle exactly. Verification (this repo, this commit's tree): scripts/generate_swaig_payloads.py --check -> exit 0, GEN-FRESH match (idempotent) scripts/run-format.sh -> no-op on the emitted files scripts/run-tests.sh -> 2148 passed / 0 failed on each of net8.0, net9.0, net10.0 suites/surface.py --port dotnet -> DRIFT PASS, SURFACE-DIFF PASS (SURFACE-FRESH compares against HEAD:port_surface.json, so it clears once this commit lands) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016i9TibArqEmDszG7VYEGYg
Provenance line only — the name list is unchanged from 9eed32d. Same self-referential re-stamp as d850268: the artifact records the commit it was generated from, so the commit that ADDS names cannot also carry its own SHA. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016i9TibArqEmDszG7VYEGYg
psdk d8e5787 re-vendored swaig-response.yaml from mod_openai cac4984 -> 8d6ed5e
and said so in its own commit message: the re-vendor "stales GEN-FRESH-SWAIG in
typescript, go, ruby, php and dotnet; that regen ... stays a separate, explicit
act." This is dotnet's explicit act -- `python3 scripts/generate_swaig_payloads.py`,
nothing hand-edited. 22 files regenerated, 2 changed.
What the re-vendor actually added: types on fields the previous vendoring left
untyped.
context_switch_action.system_pom / .user_pom
was: description-only "read by the engine; no predicate types it here"
now: type: object (+ properties pom/text, additionalProperties, propertyNames)
-> object? becomes Dictionary<string, object?>?, matching the reference's
`system_pom: dict[str, Any]` / `user_pom: dict[str, Any]`.
swaig_action.clear_dynamic_hints / .hangup / .stop / .stop_playback_bg
was: description-only
now: a UNION type list (boolean|string; stop_playback_bg boolean|string|integer)
-> object? becomes bool?.
The four bool? properties are a KNOWN, PRE-EXISTING generator narrowing, not a
choice made here: _type_schema_type (generate_rest.py:973) collapses a list-valued
`type:` to its first non-null member, so ["boolean","string"] -> boolean. The
python reference emits the full union (`clear_dynamic_hints: bool | str`,
`stop_playback_bg: bool | str | int | dict[str, Any] | list[Any] | None`). These
are surface-only envelope types with no deserializing consumer today, and the
scalar field TYPE is outside what the signature oracle records for these classes
(the four <Verb>Action value classes are method-less by design), so no gate sees
it. Reported for an owner ruling rather than papered over -- the fix belongs in
_type_schema_type and would ripple every port using that mapper.
Coordinated-With: porting-sdk wave6/ctor-dunder-fold
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjEro9sSs5TLq66rTzSq6Z
The re-vendor changed the spec these ports generate from and they were never regenerated, so GEN-FRESH-SWAIG went red across seven ports and SPEC-FANOUT reported them in aggregate on porting-sdk #125. Purely additive, as a legitimate re-vendor should be: SwaigAction gains the SWML action; SwaigRequest gains SWMLCall and SWMLVars. No existing value changes. The new SWML action is the same one SWAIG-COVERAGE reported the SDK could not emit, so this closes that gate too.
…oad regen The payload regen updated the generated SWAIG files but not the signature artifact that describes them, so SIGNATURES-FRESH went red on every port that carried it -- "committed_signatures.json does NOT match a fresh regen". Additive only: the new members are the SWML action and the SWMLCall/SWMLVars request fields the payload regen introduced.
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 that the shared-diff ctor/dunder fold(porting-sdk #125,
_is_folded_dunder_memberindiff_port_signatures.py) makes dead.Per
ALLOWLIST_DISCIPLINE.md:495,__init__-as-a-member is an EMISSION (exclude) row —never a surface capability difference. The fold excludes a
__init__finding only while thereference publishes a
constructionentry for that class, so the capability is stillcompared, by name, in
compare_construction. Entries for those classes were pure dead weight.Deletion only. No source changes; no allowlist, omission, addition, or divergence entry
added.
PORT_OMISSIONS.mdandPORT_ADDITIONS.mdare untouched.Counts
PORT_SIGNATURE_OMISSIONS.mdlinesPORT_SIGNATURE_OMISSIONS.mdentries__init__; 0 non-init)Diff vs
mainis one file:1 file changed, 7 insertions(+), 68 deletions(-)— exactly55 entry-line deletions (
git diff origin/main HEAD | grep -c '^-signalwire'→ 55) plusthe documentation cleanup below.
Documentation cleanup (not a gate fix — no gate enforces this)
The prune orphaned prose that no longer describes any entry. No gate catches this, so it is
cleaned up here in the same PR:
## RELAY event class __init__ — payload-populated data-object idiomsection(header + 8 lines of prose). All 24
signalwire.relay.event.*Event.__init__entries itexisted to explain were in the pruned set; after the prune its only surviving mentions of
signalwire.relay.event.*andFromPayloadwere inside the dead prose itself.AgentBase(AgentOptions)andRelayClient(RelayClientOptions), both now-deleted ctorentries. The bullet is still load-bearing (4 live entries cite it), so it stays — now
illustrated by
Context.AddStep/Step.SetGatherInfo, with a note on where the ctor caseswent.
Pre-existing, left alone: the
var_keyword ↔ optional<dict<string,object>>bullet says"CallingNamespace methods exhibit this," but
origin/mainalready has zerosignalwire.rest.calling.*entries — it was orphaned before this PR, by the retired-namespacechange, not by the fold. Reporting rather than sweeping it under this PR's rationale.
Construction node is unchanged
__init__-as-a-member and the §10 construction params are DIFFERENT contracts; this prunetouches only the former.
port_signatures.jsonconstructionclassesconstructionclassesConfirmed across a full re-enumeration (
scripts/enumerate_signatures.py, which takes--out—
enumerate_surface.pyis the--outputone):132 modules, 1274 classes, 2415 methods,and
port_signatures.jsoncame back byte-identical, so a fresh regen leaves the tree clean.Excused-divergence: fold-delta and prune-delta are separate
66d351a^)66d351a^)post-fold runs both read 1424 whether or not the ledger lists the symbols.
fold
continues atdiff_port_signatures.py:940, before the excusal branch at:945,so a folded ctor was contributing zero excusals even prior to deletion.
Gate output actually seen
Post-fold differ with all three surface flags — exit 0:
scripts/drift.sh(the real gate path) — exit 0,==> DRIFT exit=0. dotnet has no.drift-numeric-monotypemarker, so--numeric-monotypeis not in play here.Fold activity was confirmed against the differ FILE rather than a branch name
(
grep -c _is_folded_dunder_member scripts/diff_port_signatures.py→ 2, andgit diff wave6/ctor-dunder-fold -- scripts/diff_port_signatures.py→ 0 bytes).Mutual-dependency proof — pre-fold differ (
66d351a^) against this PR's pruned ledgerexits 1 with 61 signature drifts over exactly the 55 pruned symbols:
Full
bash scripts/run-ci.sh:[SURFACE] surface parity suite (SIGNATURES/DRIFT/SURFACE-FRESH/SURFACE-DIFF/SEMVER-DIFF/GEN-TYPE-DEGENERACY/ROUTE-COLLISION/GEN-IDIOM) ... PASS, plus LEDGER / NO-CHEAT / GEN / DOC-TRUTH / TYPE-EROSION / PACKAGE / ROOT-HYGIENEand the rest of the PR tier — all PASS, no FAIL.
__init__entries the rule does NOT cover — deliberately KEPTdotnet has 2, and both are a different mechanism from the other lanes':
signalwire.core.agent.tools.inferred_schema.InferredSchema.__init__signalwire.swml.verb_info.VerbInfo.__init__These are port-only ADDITIONS. Their modules are absent from the reference oracle's
modulesnode entirely, so their classes can never appear in referenceconstruction— thefold structurally cannot cover them, and the member entry is the only check they have.
Removing them would trade a visible ledger entry for a real blind spot, which is exactly what
the rule forbids.
Proven load-bearing, not asserted: strip just these two and the already-folded differ
goes red, so each entry is the sole thing excusing a real
extra-portfinding —dotnet carries neither cross-lane pattern: no skills-trio ctor entries
(
ApiNinjasTriviaSkill/PlayBackgroundFileSkill/WeatherApiSkill— zero hits), and noretired-namespace trio (the single
CallingNamespacehit is prose in the file header, not anentry).
🤖 Generated with Claude Code
https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
Coordinated-With: porting-sdk@wave6/ctor-dunder-fold