Skip to content

wave6: retire dead ctor entries (ALLOWLIST_DISCIPLINE §495, shared-diff fold) - #66

Open
mjerris wants to merge 76 commits into
mainfrom
wave6/ctor-dunder-fold
Open

wave6: retire dead ctor entries (ALLOWLIST_DISCIPLINE §495, shared-diff fold)#66
mjerris wants to merge 76 commits into
mainfrom
wave6/ctor-dunder-fold

Conversation

@mjerris

@mjerris mjerris commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Retires the PORT_SIGNATURE_OMISSIONS.md entries that the shared-diff ctor/dunder fold
(porting-sdk #125, _is_folded_dunder_member in diff_port_signatures.py) makes dead.
Implements ALLOWLIST_DISCIPLINE.md:495ctor/dunder → EMISSION (exclude); never a
surface capability difference
.

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.

Before / after

file before after delta
PORT_SIGNATURE_OMISSIONS.md (entries) 351 289 −62
PORT_SIGNATURE_OMISSIONS.md (__init__ entries) 62 0 −62
PORT_OMISSIONS.md / PORT_ADDITIONS.md untouched untouched 0
$ git show HEAD~1:PORT_SIGNATURE_OMISSIONS.md | grep -cE '^[A-Za-z0-9_.]+: '
351
$ grep -cE '^[A-Za-z0-9_.]+: ' PORT_SIGNATURE_OMISSIONS.md
289
$ grep -cE '^[A-Za-z0-9_.]+\.__init__:' PORT_SIGNATURE_OMISSIONS.md
0

Diff is one file, 62 deletions, zero source changes (git diff origin/main --stat).

Excused-divergence delta

Measured by running the gate with the pre-fold differ (756ea55, the commit before the
fold) against the post-fold one:

differ ledger excused exit
pre-fold unpruned (351) 6415 0
post-fold pruned (289) 6349 0
pre-fold pruned (289) 1 (FAIL)

Delta −66, and it decomposes exactly: 62 entries deleted here, plus 4 folded
ctor symbols that were previously excused via PORT_OMISSIONS.md rather than the
signature ledger (AgentBase, SkillBase, SWAIGFunction, RelayClient, RestClient
5 named there, 1 of which already matched on both sides so was never counted as excused).
Those surface entries are deliberately left in place (they belong to a different tool
with a hard dead-entry gate).

The third row is the load-bearing check: the pruned ledger fails against the pre-fold
differ, proving these 62 entries were genuinely live before the fold and are dead only
because of it — not entries that were redundant all along.

Construction node unchanged

__init__-as-a-member and the §10 construction contract are different contracts; the fold
only excludes the former, and only while the latter still covers the class.

java port_signatures.json construction classes BEFORE: 125
java port_signatures.json construction classes AFTER:  125

After ./gradlew --no-daemon build -x test + python3 scripts/enumerate_signatures.py --out ./port_signatures.json, git diff --stat port_signatures.json is empty — the
committed artifact is byte-identical, provenance line included.

__init__ entries the rule does NOT cover

None in this port's ledger — all 62 classes are present in the reference construction
node (0 uncovered).

Fleet-wide, 3 ctor symbols stay LIVE because their class has no construction entry, and
java has no ledger entry for any of them (so nothing needed keeping):

  • signalwire.core.pom_builder.PomBuilder.__init__
  • signalwire.core.swml_handler.VerbHandlerRegistry.__init__
  • signalwire.skills.registry.SkillRegistry.__init__

Also confirmed: 0 non-__init__ dunder symbols exist across the reference ∪ java
surface, so the fold's outright-exclusion branch retires nothing here.

Gate output

$ 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
✓ signatures match (1528 reference symbols, 7123 port symbols, 6349 excused divergences).
exit 0

All three surface flags, per CLAUDE.md §5b.

Full bash scripts/run-ci.sh: ==> CI PASS (exit 0) — SURFACE, GEN, BEHAVIORAL,
TEST, PACKAGE, AI-CHAT, DOC-TRUTH, FMT, LINT all PASS.

One transient red, root-caused (not a flake)

The first run-ci.sh run failed BEHAVIORAL-WIRE-RELAY (exit 1). Mechanism: java's
wire-relay dump is the Gradle task ./gradlew --quiet --console=plain wireRelayDump,
and diff_port_wire_relay.py requires its stdout to be pure JSON (it fails on "empty
or non-JSON stdout"). Under run-ci.sh the SURFACE suite rebuilds the JAR in the same
build/ dir, and Gradle progress/dependency output injected into that stdout corrupts the
JSON payload. Evidence:

  • the rule PASSes standalone (--rules BEHAVIORAL-WIRE-RELAY, exit 0);
  • the identical 19-rule invocation from the run-ci line PASSes on its own (exit 0);
  • a clean full run-ci.sh re-run is CI PASS (exit 0);
  • 6 concurrent JVMs were live on the machine (sibling ports building) at first-run time.

This is a pre-existing stdout-purity fragility in the shared harness under concurrent
Gradle load, unrelated to this PR — whose entire diff is one markdown file. Flagging it
because it will intermittently bite the other 7 wave-6 port PRs the same way; the durable
fix is for the dump tasks to write JSON to a file (or a dedicated fd) rather than share
stdout with Gradle.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf

Coordinated-With: porting-sdk@wave6/ctor-dunder-fold

mjerris and others added 30 commits July 27, 2026 01:28
Wave 6 item 1 for java: retire the dead `logger` entries in
PORT_SIGNATURE_OMISSIONS.md, plus the one further entry that folding
`get_logger` at the enumerator made unnecessary.

PORT_SIGNATURE_OMISSIONS.md: 351 -> 346 entries (-5)
PORT_OMISSIONS.md:            39 ->  39 (unchanged)
PORT_ADDITIONS.md:           579 -> 579 (unchanged)

Counts measured with the gate's OWN parser
(diff_port_signatures.parse_omissions), on the committed tree.

## The 4 dead `logger` entries (item 1, settled policy)

  signalwire.agent_server.AgentServer.logger
  signalwire.core.skill_base.SkillBase.logger
  signalwire.core.skill_manager.SkillManager.logger
  signalwire.skills.registry.SkillRegistry.logger

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 a port may reach however its language does; the
per-instance `logger` attribute is Python's structlog idiom leaking into the
enumerated surface and is not contract.

These 4 are dead paperwork, not load-bearing exemptions. Verified against the
oracle: `query_signatures.py python_signatures.json search logger` returns ONLY
`modules.signalwire.core.logging_config.functions.get_logger` — the module-level
factory — and no class-attribute `logger` anywhere. The "excused divergences"
count is unchanged at 6415 across their removal, which is the direct proof they
were excusing nothing.

## The 5th entry: get_logger, FOLDED at the enumerator (not merely deleted)

  signalwire.core.logging_config.get_logger

This one was NOT dead — the oracle does still emit it, so item 1's rule did not
cover it. Its rationale said the member was "covered at the surface layer
(rename/projection)", and that named a real asymmetry:

  * scripts/enumerate_surface.py already projected ("Logger","getLogger") ->
    signalwire.core.logging_config.get_logger (line 670).
  * scripts/enumerate_signatures.py did NOT — the corresponding line was missing
    from FREE_FUNCTION_PROJECTIONS, even though the three sibling logging free
    functions (configureLogging / resetLoggingConfiguration / stripControlChars)
    are all present in the same block, under a comment stating the table
    "mirrors _FREE_FUNCTION_SURFACE_PROJECTIONS in enumerate_surface.py".

The two tables were out of lockstep by exactly one line, and the omission entry
was paperwork covering that drift. Fixed by adding the missing projection rather
than by keeping the exemption.

Why the projection alone is not enough: `Logger.getLogger` is OVERLOADED
(`String name` / `Class<?> clazz`) and both overloads have arity 1, so the
generic fewer-param overload-collapse cannot separate them — the tie-break
picked the `Class<?>` one and emitted `get_logger(clazz: any)`, which does not
match the oracle's `get_logger(name: string) -> any`. The canonical shape is
therefore pinned via FREE_FUNCTION_SIGNATURE_OVERRIDES, the mechanism already
used for exactly this class of problem (e.g. WebhookValidator.validate). The
`Class<?>` overload stays a Java-idiom convenience the collapse drops.

Result: java now satisfies all 5 module-level logging free functions the ruling
names as signalling the capability — get_logger, configure_logging,
get_execution_mode, reset_logging_configuration, strip_control_chars — as
module-level functions, where before it satisfied only 4.

No PORT_ADDITIONS entry was added, deleted, or exempted by this change. The 12
`Logger.*` + 2 `LoggerLevel.*` additions are untouched, pending the owner's
RETURN-CONTRACT question (A_PLUS_CAMPAIGN_PLAN.md, blind spot 2).

## Regenerated artifacts

port_signatures.json regenerated after `./gradlew --no-daemon build -x test`
(the adapter reads the JAR, so a rebuild must precede enumeration): 7101 ->
7100 methods, as `Logger.get_logger` moves from a class method to a
module-level free function.

port_surface_native.json is refreshed content-wise (AiSidecar /
AiSidecarConfig / RingbackConfig) — pre-existing staleness from the 39-verb
schema pass, unrelated to this change. Confirmed unrelated by regenerating both
surface artifacts from pristine main and byte-comparing: this change does not
affect the surface layer at all, since the surface enumerator already carried
the projection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
`--output` defaulted to STDOUT. A bare run therefore printed the snapshot to a
stdout the caller discarded, wrote NOTHING, and exited 0 — so a clean `git status`
afterwards read as "no change was needed" when it actually meant "nothing was
written". Demonstrated on this repo before the fix:

    == BEFORE ==
    port_surface.json md5: 55cdd0861e65722e57af8aabba174499
    git status --short port_surface.json: []

    == BARE RUN: python3 scripts/enumerate_surface.py  (stdout discarded) ==
    exit code: 0

    == AFTER ==
    port_surface.json md5: 55cdd0861e65722e57af8aabba174499
    git status --short port_surface.json: []

This already cost a 7-port surface-audit red, and it is invisible precisely when it
matters. An output destination is now MANDATORY: `--output PATH` writes the file,
the new `--stdout` is the explicit opt-in for the pipe, and a bare run fails loud
with a usage error (exit 2).

Fail-loud rather than "make the file the default" because it cannot silently change
an existing caller's behaviour: a caller that today relies on the stdout default and
redirects gets an immediate, self-describing error instead of quietly stopping
producing output on the pipe. It also matches the fail-loud doctrine used elsewhere
in this campaign. (For reference, the file IS the default across the rest of the
fleet — go/php/rust/cpp/dotnet/ts/perl all write it, and go/cpp/dotnet/ts/perl
already offer `--stdout` as the explicit opt-in. This change makes Java stop being
the fleet's one exception, via the stricter of the two routes.)

Every existing caller already passes `--output` explicitly:
`scripts/generate_exemptions.py:916`, `.github/workflows/surface-audit.yml` (both
steps), `.github/workflows/doc-audit.yml` (both steps, with `--native`). The ONLY
bare invocation in the fleet is porting-sdk's SURFACE-FRESH/SURFACE-DIFF driver,
which is fixed in the coordinated PR below.

Also emits a `wrote <path>` line to stderr on success, matching php/rust/cpp/dotnet/
perl/ts.

Coordinated-With: signalwire/porting-sdk#<PSDK_PR>
Merge-order: THAT PR FIRST, then this one.

**This PR's CI is RED until the porting-sdk PR merges.** This repo's CI clones
porting-sdk at `vars.PORTING_SDK_REF || 'main'`, and porting-sdk's
`scripts/suites/_surface_fresh.py` on `main` still invokes this enumerator BARE with
a shell redirect. Against a fail-loud enumerator that invocation now errors, so
SURFACE-FRESH and SURFACE-DIFF fail `exit 2`. With the porting-sdk branch checked
out adjacent, java's SURFACE suite is 9/9 PASS. That is a merge-order dependency,
not a defect in this change.

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 `__init__` as a MEMBER whenever the
reference publishes a `construction` entry for that class — the capability is
still compared, by NAME, in compare_construction. That makes the signature
ledger's blanket ctor entries dead: the gate no longer consults them.

Deletes the 62 `PORT_SIGNATURE_OMISSIONS.md` entries the fold makes dead
(351 -> 289 entries). All 62 classes verified present in the reference
`construction` node; 0 uncovered.

`construction` node untouched: 125 classes before and after, and
port_signatures.json is byte-identical after a rebuild + re-enumerate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf
The signature oracle now records 7 DERIVED public __init__ attributes that
were previously invisible to the contract (porting-sdk d7c859d). Per the
2026-07-27 ruling (ALLOWLIST_DISCIPLINE.md class B2) a derived attribute is
contract when it is a caller-observable VALUE, so each is expressed here in
the Java accessor idiom.

Already present, cleared by rebuilding the JAR (the adapter reads the JAR, so
the committed port_signatures.json was stale against its own source):
  - SignalWireRestError.request_id -> RestError.getRequestId()

New surface:
  - SWMLService.ssl_enabled/domain/ssl_cert_path/ssl_key_path ->
    Service.isSslEnabled()/getDomain()/getSslCertPath()/getSslKeyPath(),
    delegating to the one SecurityConfig. The reference reads these off
    self.security in __init__ (swml_service.py:143-146); reading through
    rather than copying keeps a later loadFromEnv()/config reload reflected,
    which is how the reference's own start() re-reads them (:1240).
  - Action.completed -> Action.getCompleted(). The reference exposes BOTH the
    `completed` attribute AND an is_done() method over the same state, so
    this port keeps isDone() and adds the attribute form; both read the one
    `done` field.
  - SpiderSkill.remove_xpaths -> getRemoveXpaths()/setRemoveXpaths(), a
    PREFILLED list carrying the reference's default expressions verbatim
    (spider/skill.py:191-199). It is load-bearing, not decorative: the scrape
    path now strips the elements it names (previously script/style were
    hardcoded in the regex), and setup() accepts a remove_xpaths param.

Verified: signature DRIFT clears all 7; full suite 2219 tests green.

KNOWN, NOT INTRODUCED HERE — two gates stay red on pre-existing state:
  1. RestClient.project return-mismatch (ProjectNamespace vs string). Java's
     getProject() returns the credential project-ID string and there is no
     namespace accessor. Reproduced on a pristine tree with none of this
     change; it surfaced because ff3267d's accessor fold plus the JAR rebuild
     exposed drift the stale committed artifact was masking.
  2. The 7 new accessors read as SURFACE additions because d7c859d updated
     python_signatures.json ONLY — enumerate_python.py (which builds
     python_surface.json) has no derived-attr extraction, so the two oracles
     are out of lockstep for exactly these symbols. Deliberately NOT papered
     over with PORT_ADDITIONS entries (AGENT_RULES §3.0: an agent may not
     create an addition, and "accessor idiom" is a self-refuting rationale).
     The fix belongs upstream in the surface oracle.
…earing

The PRODUCT fix already landed in 6e6f78a (removeXpaths promoted to a prefilled
caller-observable field carrying the reference's seven //tag entries, driving the
strip loop). What was missing was a test that would notice if it regressed.

(Orchestrator note: I reported java as still leaking. That was MY error — I grepped
~/src/signalwire-java while the lane's commit lives in a worktree. Java was never
missed; remove_xpaths was absent from its drift list because it was already closed.)

5 new tests (5 -> 10), driven by a loopback HttpServer fixture so the scrape path
runs offline over a real socket rather than a stubbed fetch.

MUTATION-TESTED two independent ways, both RED:
  - revert to the original hardcoded script/style regex -> 2 tests fail
  - keep the field-driven loop but narrow the list to script|style -> same 2 fail,
    verbatim "removed element's text content leaked into scraped output: NAVTEXT",
    output "HEADERTEXT NAVTEXT ASIDETEXT NOSCRIPTONLYTEXT KEEPTEXT FOOTERTEXT"

Fixture detail worth preserving: the sentinel is NOSCRIPTONLYTEXT, not NOSCRIPTTEXT.
The latter CONTAINS "SCRIPTTEXT", so a noscript leak masquerades as a script leak and
the two element types cannot be told apart by a substring assertion.

Drift unchanged at exactly 1 (the pre-existing RestClient.project return-mismatch);
port_signatures.json byte-identical to HEAD. Spider 10/10, FMT 0, LINT 0.
…accepted as ready

MY TOCTOU DIAGNOSIS WAS WRONG. I filed this as a port race between concurrently-picked
free ports. It is not a race at all: the 547 failures are DETERMINISTIC and have nothing
to do with ports, concurrency, or JUnit scheduling. The lane refused the brief and
measured instead.

ACTUAL ROOT CAUSE — a wrong-PACKAGE resolution, not a wrong-SERVER race:
MockTest.discoverPortingSdkPackage() walks up from user.dir looking for a SIBLING
porting-sdk/. The wave branch lives in a worktree under the session scratchpad, where no
porting-sdk exists up that chain. Discovery returns null, PYTHONPATH is not injected, and
`python3 -m mock_signalwire` falls back to a stale copy in an unrelated venv
(~/src/signalwire-agents/venv/.../site-packages/mock_signalwire). VERIFIED: that copy has
no rest-apis/ tree, so it loads 0 specs / 0 routes — and still answers health with
HTTP 200 {"status":"ok","specs_loaded":0,...,"total_routes":0}.

The probe was `text.contains("\"specs_loaded\"")` — KEY PRESENCE, NOT VALUE. The empty
mock was accepted as ready, every REST call 404'd, and the failures surfaced against SDK
code rather than the harness.

WHY MY EVIDENCE MISLED ME (each datum fit the wrong theory):
 - "547 on branch vs 0 on baseline from a test-only change" — the variable was WHERE the
   branch was checked out, not the branch.
 - "no-op tests reproduce 547 EXACTLY" — I read invariance as scheduling re-rolling a
   race. It is the opposite: the count is invariant BECAUSE there is no race.
 - "a manual mock on that port served 236 routes" — started by hand from ~/src, so it
   resolved the CORRECT package. Not a wrong server; a wrong package.

CONTROL, reversible both directions: worktree as-is -> 547 failed; add a sibling
porting-sdk symlink -> BUILD SUCCESSFUL; remove it -> exactly 547 again.

FIX (test-only): readiness now requires specs_loaded > 0 AND total_routes > 0 (and
schemas_loaded > 0 for RELAY — the same hole existed there). Startup failure quotes the
health body verbatim plus the discovery result, so the error names the stale venv path
outright instead of blaming the SDK. New MockHealthContractTest (12 tests) pins the
contract as pure predicates — no server, no ports, no sleeps.

NOTHING WAS SERIALISED: no @execution(SAME_THREAD), no @ResourceLock,
junit-platform.properties untouched.

Mutation RED both sides: old REST probe -> 6 failures; old RELAY probe -> 2 failures.
3x consecutive full-suite runs all exit 0. Drift exactly 1 (pre-existing
RestClient.project); port_signatures.json unchanged. SURFACE + SECURE-DEFAULT reds
proved pre-existing by stashing and re-running on the pristine tip (byte-identical set).

Refs #92
…ator to see ResourceTree

Java was the outlier at 0 of 23 RestClient members — not a capability gap. RestClient.java:31
is `public class RestClient extends ResourceTree`, the same mechanism as every other port,
and the generated base declares all 22 accessors. All 22 proven reachable against the
shared mock before any change (ResourceTreeReachabilityMockTest, 5 tests).

TWO STACKED CAUSES, both enumerator-side — java was the only port doing both:
 1. SignatureDump.java:189 walks getDeclaredMethods() only, so base-declared accessors are
    invisible on the subclass. RestClient's ctor is private, so no <init> either.
 2. enumerate_signatures.py:1348 then DELETED the ResourceTree entry outright, with the
    comment "covered by PORT_ADDITIONS".
Invisible on the subclass AND deleted on the base = 0. The SURFACE enumerator already
retargeted correctly, which is why only the signature axis looked broken.

THE SERIOUS FINDING — 23 PORT_ADDITIONS entries were LAUNDERING MATCHED SURFACE.
Their rationale claimed "the reference has no method-surface counterpart". That is FALSE
and mechanically checkable: the oracle records 40 RestClient members, and all 22 accessors
appear in BOTH python_surface.json and python_signatures.json. Each was audited and found
present in both port and reference. Deleted, plus 1 genuinely dead get_project entry.
24 deleted, 0 added; PORT_OMISSIONS and PORT_SIGNATURE_OMISSIONS untouched.

This is the exact shape the rules forbid: an exemption that makes real drift invisible
rather than fixing the emitter. It also masked the port's ONE remaining drift —
RestClient.project resolved to the credential getter getProject(): String because the real
project() returning ProjectNamespace lived on the deleted base. Same root cause, no
separate fix needed.

FIX: retarget ResourceTree onto signalwire.rest.client.RestClient (mirroring the surface
enumerator) plus a role-aware merge — precedence by role, not arrival order, since
reflection's type order is unspecified. RestClient 0 -> 27 members, deterministic across
3 runs.

Also regenerated port_surface.json: the committed copy carried `generated_from … c485c73`
— a DIFFERENT worktree's detached HEAD (consol-java), predating this branch's getter->field
fold. That stale artifact was the whole of the 7 unexplained getter/field pairs
SURFACE-DIFF was reporting.

Verified independently by the orchestrator with real files (not process substitution):
  signatures match (1557 reference symbols, 7166 port symbols, 6360 excused)
  port matches Python reference (2767 symbols; 39 excused omissions, 555 excused additions)

Pre-existing and untouched: SECURE-DEFAULT (Wave-1 SWAIG dump format, 72a989e).
The reference defaults `token_expiry_secs` to 900 — 15 minutes
(core/security/session_manager.py:30). java's no-arg constructor used 3600,
quadrupling the window in which a leaked tool token stays valid. Owner ruling
2026-07-27; four ports had drifted (ruby/java/rust/dotnet) while five already
matched (go/ts/php/perl/cpp).

Only the DEFAULT moves. `new SessionManager(n)` is untouched, and a regression
test pins that an explicitly-passed value — including the old 3600 — still passes
through.

Deliberately NOT swept repo-wide: the reference has TWO different defaults that
disagree on purpose. `AgentBase(token_expiry_secs=3600)` (agent_base.py:130) is
the intended public default for an agent and passes its value to SessionManager
explicitly (agent_base.py:247), so java's AgentBase 3600 and the assertion in
AgentConstructionParamsTest.tokenExpirySecsDefaultsTo3600 are CORRECT and are left
alone. Because AgentBase always passes its value through, this change cannot alter
agent wire behaviour.

testDefaultExpiry previously only minted a token and validated it, which passes at
ANY default and so could never have caught the divergence; it now asserts the
value. Confirmed by mutation: restoring 3600 fails that assertion while
AgentConstructionParamsTest stays green, so the two defaults are independently
covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…aig callback

java emitted `web_hook_url` on EVERY rendered SWAIG function. The live path
(AgentBase.buildSwaigFunctions -> SWAIGFunction.toSwaig) built
`baseUrl + "/swaig"` and guarded emission on `!url.isEmpty()` — a condition that
is never false — so a tool declared `secure=false` was published with a bare,
unauthenticated function-specific callback. The reference emits NO key at all in
that case (agent_base.py:1085-1099).

Three parts:

1. The per-tool guard now matches the reference's three-way branch: an external
   webhook URL passes through untouched, a locally-handled tool gets its own
   webhook ONLY when it carries a token or the agent has SWAIG query params, and
   an insecure locally-handled tool gets no per-tool entry at all. The security
   token rides as the `__token` QUERY PARAMETER (agent_base.py:1096-1100), not as
   `meta_data_token` — schema.json defines that as the "Scoping token for
   meta_data", the engine MD5-derives it when absent
   (mod_openai/app_config.c:1031-1042) and uses it only to key the per-function
   metadata store (actions.c:2085-2093). Nothing validates it as a credential, so
   the token placed there left the callback unauthenticated AND mis-scoped the
   metadata store.

2. `SWAIG.defaults.web_hook_url` is now emitted whenever functions exist
   (reference agent_base.py:1108-1113). This half was MISSING: AgentBase built its
   swaig map inline with only functions/includes/native_functions/mcp_servers,
   while the one code path that did emit defaults (SwmlRenderer.buildSwaigConfig)
   is dead for the agent — its only caller is SwmlRenderer.java:177. It is
   load-bearing rather than cosmetic: because an insecure tool now correctly
   renders no per-tool webhook, defaults.web_hook_url is the ONLY endpoint it has.

3. SecureDefaultDump emitted a self-echoing vacuity — `classify()` did
   `m.put("secure_default_true", expectedSecure)`, echoing its own parameter, so
   the field could never fail. THAT is why java passed SECURE-DEFAULT green while
   shipping an unauthenticated callback. It now emits the rendered wire payload
   with token VALUES redacted and every KEY preserved, and makes no judgement of
   its own; the differ classifies.

Verified by mutation, not just by a green run: reverting the per-tool guard makes
the SECURE-DEFAULT gate fail with "expected NO per-tool web_hook_url on this
entry", and dropping the defaults block fails the new AuthTest.

Note for the gate owner: SECURE-DEFAULT inspects only per-function entries, so it
still PASSES with `SWAIG.defaults` missing entirely. The AuthTest added here is
currently the only coverage of that half.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…uired-flip

The unified drift checker (porting-sdk 90164e9) now compares `required` on every
param, not just `__init__` ones, and java came back with 105 findings — 100%
required-flip, the only port with that shape. That uniformity was the clue: it is
one systematic cause, not 105 unrelated gaps.

Root cause: scripts/enumerate_signatures.py build_signature() hardcoded
`"required": True,  # Java has no defaults` for EVERY reflected param. Java has no
default arguments, but its idiomatic substitute — METHOD OVERLOADING — does express
optionality, and reflection reports every overload. The enumerator was throwing that
information away, so a param the source really does treat as optional was recorded
as required.

Split of the 105, measured against the reflection dump rather than assumed:

  67  the shorter overload ALREADY EXISTED — e.g. Action.pause() delegates to
      pause(null) and the reference default is None. Pure enumerator defect.
  38  no overload omitted the param — a real gap in the port.

Both halves are fixed, and neither is papered over:

* optional_param_names() derives optionality from the overload set: a param is
  optional when some shorter PREFIX overload of the same method omits it. Only
  trailing-omission overloads count, so a same-arity type-swap (tap's enum-vs-String
  forms) is not misread as an optionality claim. No `default` value is synthesized —
  reflection cannot see the constant a short overload forwards, and the checker
  treats optional-without-default as unrecorded, not drift. Inventing values would
  have been the dishonest half.

* The 38 genuine gaps get the overload they were missing, each delegating with THE
  REFERENCE'S default, cited by file:line in its javadoc. No existing signature
  changed — every addition is a new overload, so no caller breaks.

* VARARGS_HEAD_OPTIONAL covers signalwire.RestClient's leading `*args` door, the
  var-positional twin of the existing KWARGS_TAIL_OPTIONAL trailing-`**kwargs` rule.

One regression caught and fixed in passing: the new short overloads initially WON
java's fewest-param overload collapse, so the recorded surface became the short form
and 20 required-flips silently became param-count-mismatches — strictly worse. Those
20 symbols are now in PREFER_FULL_OVERLOAD (plus a free-function twin table for the
projected validate_url), keeping the full-arity signature as the parity surface with
its params correctly marked optional.

Verification:
  required-flip     105 -> 0
  every other kind  unchanged vs baseline; 0 new findings of ANY kind
  run-ci            exit 0, 23 gates PASS

Tests: 25 new behavioural tests (20 unit + 5 mock-relay). Each calls the SHORT form
and asserts it matches the LONG form invoked with the reference default written out
explicitly — so forwarding a wrong constant fails the test, which a
long-form-only or literal-expectation test would not. Mutation-checked: hold's 300
-> 301 and validate_url's false -> true each turned exactly their own test red and
nothing else.

Wire divergence found, reported NOT worked around: the reference declares
Call.user_event(event=None) but relay-protocol/calling.user_event.params.json (from
switchblade PublicCallUserEventParams.cs) lists `event` as required, so the
reference's own default is unsendable — the server answers -32602. The test pins
that behaviour rather than hiding it; adjudicating reference-vs-wire is out of scope
for this change.
…sed 6028 -> 6021

Wave B: retire the rename / "no analog" entries in PORT_SIGNATURE_OMISSIONS.md.
A rename is reconciled in the adapter, never an omission — an omission is a
permanent blind spot, a rename keeps comparing. 10 entries deleted; 7 of the
symbols they excused now compare EQUAL, 3 excused nothing at all.

THE ENUMERATOR DEFECT (3 entries): enumerate_signatures.py DROPPED any method
whose canonical snake name is a Python keyword (`if method_canonical in
_PY_KEYWORDS: continue`) instead of applying the keyword escape. The surface
enumerator has always done the right thing (`if snake in _PY_KEYWORDS: snake +=
"_"`), and _METHOD_RENAMES is even KEYED on the escaped form — its `await_` ->
`wait` entry documents exactly this order, and could never fire because the
signature enumerator discarded the name first. So:

  * Call.pass_ — the entry claimed "Java exposes the same method as
    call.transfer()". False: the reference has BOTH pass_() and transfer(dest),
    and they emit two DIFFERENT RELAY RPCs (calling.pass vs calling.transfer,
    both in relay-protocol/). Java's Call.pass() existed and worked the whole
    time; only the enumerator could not see it.
  * Action.wait / Message.wait — the Java source's own javadoc asserted "the
    enumerator's rename table maps await -> wait". The table entry exists; the
    keyword `continue` meant it never ran.

Enums are excluded from the escape: an enum CONSTANT is not a method, so
ExecutionMode.LAMBDA must not become `lambda_`.

THE CAPABILITY GAP (4 entries): the wait_for_* entries claimed "no relay Call
state-wait primitive in the Java port", citing wait_for/wait_for_ended as
"omitted in PORT_OMISSIONS.md as having no direct Java analog". Both premises
are false — they are not in PORT_OMISSIONS.md, and Call.waitFor/waitForAnswered/
waitForRinging/waitForEnding/waitForEnded all exist. The real divergence was
narrow: waitFor(target, timeoutMs) already carried a timeout and the four typed
wrappers just did not expose it. Added the overloads (+ Action/Message await),
typed `Double timeout` in SECONDS to match the reference's
`timeout: float | None = None` and the port's own RequestOptions.timeout
spelling, with PREFER_FULL_OVERLOAD entries so the collapse keeps the full
signature. This also resolved wait_for_ended, which was outside the surveyed
bucket but the same defect.

DEAD ENTRIES (3): pom.Section.add_body/add_bullets/to_dict suppressed nothing —
both sides already record all three identically. Pure blind spot for no benefit.

BEHAVIORAL-WIRE-RELAY (found while verifying, fixed here): WireRelayDump.waitConn
returns as soon as the mock ACCEPTS the socket, which is not the handshake.
RelayClient.execute buffers a request whenever `connected` is false, and that
queue is drained only by a RECONNECT — which never happens in the dump — so a
calling.play issued in that window hangs until the 30s execute timeout. Added
waitReady(isConnected). Reproduced red once under load, 5/5 green after.

Verification: run-ci exit 0, 22/22 gates PASS. Drift 324 -> 326, excused
6028 -> 6021. Param-property drift stays ZERO (no required-flip, no
param-count-mismatch) — the ea7e0ba baseline holds. The +2 are
ExecutionMode.lambda and ServerlessSimulatorPlatform.lambda, enum constants the
keyword `continue` had been hiding; both classes are absent from the reference
entirely and their 11 sibling constants were already unexcused drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…changelog

Owner rulings 2026-07-28, completing the fleet:
  - build.gradle 4.0.0 -> 3.0.0
  - port_signatures.baseline.json baseline_version 3.0.2 -> 3.0.0 with the
    recorded surface payload overwritten from the current port_signatures.json
    and the anchor re-pointed at HEAD (setting baseline_version alone leaves
    SEMVER-DIFF demanding 'major', since it diffs the surface against the floor)
  - CHANGELOG: collapse 4.0.0/3.2.0/3.1.0/3.0.2 into one 3.0.0. Nothing above
    v2.0.2 was ever published, so these were never a release history. All 12
    bullets preserved, including the BREAKING note.

semver_diff: actual bump = 'none', required = 'none' [ok]
meta_consistent: clean
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

generateToken base64url-encoded the token with `.withoutPadding()`. 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 though the message and the HMAC were correct — in production every
secure tool call fails authentication.

Our own validateToken kept accepting them because `Base64.getUrlDecoder()` tolerates
missing padding. That encoder/decoder asymmetry is exactly 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: a
token this port MINTS validates under the reference's decoder). SECURE-DEFAULT proves
a token is minted and the keying check proves the HMAC key; neither sees the base64
ENVELOPE. Per-PR rather than nightly — a security property should not wait — and on
its own sched_gate line rather than joining the defer=1 BEHAVIORAL suite.

createSession keeps `.withoutPadding()`: it mirrors `secrets.token_urlsafe(16)`,
which is genuinely unpadded. Only the token envelope changed.

Verified: gate exit 0 with the fix; reverting just the encoder call reproduces
"base64 envelope is not decodable the way the reference decodes it / urlsafe_b64decode
raised Error('Incorrect padding')" — so the gate fails for the right reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…er RFC 7235

Two defects on the bearer path, both proven by test:

1. BearerCredentials dropped half its contract. The reference re-exports FastAPI's
   HTTPAuthorizationCredentials under this name and records BOTH `scheme` and
   `credentials`; this port carried only the token, so a caller could not tell which
   scheme the client had actually sent. Now `record BearerCredentials(String scheme,
   String credentials)`, matching the reference and the typescript port (AuthHandler.ts:76).

2. The scheme was compared CASE-SENSITIVELY. `header.startsWith("Bearer ")` rejected a
   conforming `bearer <tok>` / `BEARER <tok>`. RFC 7235 makes the auth-scheme token
   case-insensitive, and FastAPI's HTTPBearer — which the reference delegates to —
   lowercases before comparing. This is divergence (a) of the fleet auth sweep
   (task #142); perl, ruby, typescript and php already carry the same fix.
   bearerOk() now splits on the first space and compares with equalsIgnoreCase.

Tests: bearerSchemeIsCaseInsensitiveOnTheWire drives the real public entry point
(getFastapiDependency) with Bearer/bearer/BEARER/BeArEr and asserts all four
authenticate, while a wrong token and a `Bearerx` scheme still fail — so the relaxation
is scoped and did not make the check vacuous. bearerCredentialsCarriesSchemeAndCredentials
pins both fields. Existing call sites updated.

Verified: gradlew test --tests '*AuthHandlerTest*' -> BUILD SUCCESSFUL, exit 0;
full `gradlew build -x test` (checkstyle included) -> BUILD SUCCESSFUL.

NOT closed by this commit: java's SURFACE gate is still red on the four
auth_handler.{Basic,Bearer}Credentials members. These records are NESTED inside
AuthHandler, and java's enumerate_signatures.py walks only top-level types, so
port_signatures.json contains ZERO occurrences of either class name. That is an
enumerator gap, tracked separately — the surface fix above is a prerequisite for it,
not a substitute.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…loor payload

The committed artifact was missing the implicit default constructors of three
built-in skills — ApiNinjaTriviaSkill, PlayBackgroundFileSkill, WeatherApiSkill.
A fresh `enumerate_signatures.py` emits them, so the tracked blob no longer matched
what the enumerator produces and SURFACE-FRESH was measuring a stale file.

All three are REAL surface, verified rather than assumed:
  - the classes exist (src/main/java/com/signalwire/sdk/skills/builtin/*.java),
  - each has an implicit no-arg constructor (no explicit ctor declared),
  - the oracle records `__init__` on the matching reference class
    (signalwire.skills.api_ninjas_trivia.skill.ApiNinjasTriviaSkill -> ['__init__',
    'get_tools']),
  - typescript, ruby and php already carry the same members.

Note the name: Java's class is `ApiNinjaTriviaSkill` (no 's'), folded to the
reference's `ApiNinjasTriviaSkill` by the enumerator's rename table
(enumerate_signatures.py:606). Searching for a file named `ApiNinjasTriviaSkill.java`
finds nothing and makes the member look invented — it is not. I made exactly that
mistake and briefly discarded this regen as phantom surface before checking the
rename table.

The 3.0.0 floor payload is re-synced to the corrected surface, so SEMVER-DIFF reads
'none'/'none' again rather than reporting the three constructors as a minor addition
against a floor captured from the stale artifact. Nothing is published at 3.0.0, so
the floor is a snapshot of today's surface, not of a release (see .version-frozen).

Verified: semver_diff / meta_consistent / root_hygiene all exit 0; re-running the
enumerator a second time is byte-identical (idempotent), so the artifact is genuinely
fresh and not merely different.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…ty trio; un-stale port_surface.json

Closes java's SURFACE DRIFT red — the four
signalwire.core.auth_handler.{Basic,Bearer}Credentials members that read as
missing-port while the code implemented them correctly.

THREE causes, all in the enumerators, none in the SDK:

1. _SIG_EXCLUDED_SIMPLE_NAMES dropped BasicCredentials / BearerCredentials as
   "port-only value types with no reference counterpart". That was true only while
   griffe could not resolve FastAPI's HTTPBasicCredentials /
   HTTPAuthorizationCredentials into the `signalwire.` tree. porting-sdk dcff742
   filled them in as the real two-field classes they always were on the wire, so the
   exclusion became a self-inflicted blind spot. Removed, with the history recorded
   at the site so it is not re-added.

2. They are records NESTED in AuthHandler, so SignatureDump emits them under a bare
   simple name. Added JAVA_NESTED_CLASS_RENAMES entries mapping both into
   signalwire.core.auth_handler — where the reference records them — rather than a
   per-class module.

3. javac GENERATES equals/hashCode/toString for every record, which then surfaced as
   port-only ADDITIONS. These are the same construction/identity idiom the existing
   _CTOR_DUNDER_NAMES set already drops for Python's dunders, just spelled the JVM
   way: equals IS __eq__, hash_code IS __hash__, to_string IS __str__. Added to that
   set, so the EXISTING guard applies unchanged — dropped ONLY when the reference
   records no member of that name on that class.

The fold is guarded, not blanket: measured, ZERO reference classes record
equals/hash_code/to_string, so nothing real was suppressed. A future class whose
reference twin genuinely declares one still matches.

port_surface.json is regenerated (177 differing leaves). That staleness is task #136,
not this work: porting-sdk 8828dd2 made `__init__` emission mandatory fleet-wide and
java's committed surface predates it, so every class list was shifted by one from
index 0. The credential classes now appear there too.

Verified: DRIFT exit 0 — "signatures match (1564 reference symbols, 7171 port
symbols, 6337 excused divergences)". semver_diff / meta_consistent / root_hygiene all
exit 0 with the floor payload re-synced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…ory — SURFACE-DIFF green

Closes java's last 10 SURFACE-DIFF symbols. Three shapes, all the same root cause:
porting-sdk 8828dd2 made `__init__` emission mandatory, and this enumerator is
text-based, so construction it cannot SEE reads as missing surface.

1. ai_chat @DataClass responses (3) — ConversationInfo / ChatResponse / ChatLog. The
   oracle records `__init__` on each; _AI_CHAT_MEMBER_OVERRIDES is a FILTER, so
   omitting the name from the keep-set stripped it back out. Added.

2. Java RECORDS (6) — BasicCredentials / BearerCredentials. A record declares its
   state in the HEADER (`record BasicCredentials(String username, String password)`)
   and javac generates the canonical ctor plus one public accessor per component. The
   enumerator reads the class BODY, which is empty, so it saw neither. New
   _RECORD_SURFACE_MEMBERS table + _record_components(), which PARSES the record
   header: every non-`__init__` name must appear as a real component or nothing is
   emitted, so a stale entry cannot invent surface.

3. Private ctor behind a public factory (1) — RequestOptions has
   `private RequestOptions(Builder)` and a public `builder()`. The capability is real;
   the ctor simply is not the entry point. New _PRIVATE_CTOR_PUBLIC_FACTORY, GATED on
   the factory actually being present in the emitted member set, so a class that loses
   its builder stops claiming construction rather than silently keeping it. Same
   doctrine .NET already applies to its private-ctor singletons.

Verified: SURFACE-DIFF "✓ port matches Python reference (2803 symbols; 39 excused
omissions, 555 excused additions)" — down from 10 missing (10 -> 7 -> 1 -> 0 as each
shape landed).

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 13 -> 12  (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 java --repo . --max 12 -> exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…tch the reference shape

TWO defects, one of them live.

1. THE SCRUB WAS NEVER CALLED. `stripControlChars` was public, correct, and had
   ZERO call sites — grep the tree and the only hit is its own definition. All
   THREE emission points printed the caller's message verbatim, so a NUL, a BEL,
   or an ESC-[ escape reached the terminal intact. The reference registers this
   scrub in BOTH of its structlog processor chains (logging_config.py:205,233);
   here it was a method nobody invoked. Log-injection defence present in name only.

2. WRONG SHAPE. The reference's public contract takes the event MAP and scrubs
   every string value; this port took a single `String`. A caller could not hand it
   a log event and have the values sanitised.

  stripControlChars(String)       -> stripControlCharsValue, PACKAGE-PRIVATE
                                     (the per-value scrub, the unit the emitter
                                     needs; no longer port surface)
  stripControlChars(Map)          -> NEW public form matching the reference:
                                     scrubs string values, passes non-strings
                                     through (the `isinstance(value, str)` guard)

THREE emission sites wired, not one. `log(Level,String)` and
`log(Level,String,Object...)` are the obvious pair; `error(String,Throwable)`
prints through its OWN `System.err.printf` and is easy to miss — it is wired too.

Fixing only the signature would have turned every gate green while leaving the
emitter unprotected — a certified-correct signature in front of a live defect.

THE TEST IS THE POINT. The emission tests capture what the logger ACTUALLY writes
(swapping System.out) rather than calling the scrub helper directly, so they fail
when the wiring is removed. Verified by reverting one emission site — RED:

    logOutputHasControlCharsStripped(): NUL survived into the emitted line: [INFO] [inject.te...

A helper-only test passes against that same break, which is exactly how this
shipped unprotected.

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 (full suite). LoggingTest: 12 tests, 0 failures,
confirmed from the JUnit XML that all three new tests actually EXECUTED rather
than being silently filtered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKwgPbehDoAMdG3hPL79oi
…entry

Part of the fleet-wide false-ledger sweep. Java's slice was far less deletable than the fleet
pattern predicted — but it contained a large, systematically INVERTED block.

THREE CLAIMS IN MY OWN BRIEF DID NOT SURVIVE SOURCE, and are corrected here:
  - "PORT_SIGNATURE_OMISSIONS.md:211-212" — on_call/on_message are at 180-181; 211-212 is
    unrelated boilerplate.
  - "16 EventHandler-family entries" — no such family exists in java's ledger. There are 4
    handler-shaped entries, 2 of which (on_debug_event, on_summary) are TRUE as written.
  - "the FALSE on_call rationale" — the rationale IS false, but the divergence underneath it is
    REAL, so the entry is MISWORDED-BUT-REAL and must be corrected, not deleted.

A. 21 relay Action sub-action entries — RATIONALE EXACTLY INVERTED.
   claimed: "Java returns the payload as Map; Python returns void."
   source is the reverse:
     reference relay/call.py:138  async def stop(self) -> dict[str, Any]
     java      swml/Service.java:1685  public void stop()
   The divergence is real in the OTHER direction. Wording corrected, entries kept.

B. on_call / on_message — same inversion.
     reference relay/client.py:327  def on_call(self, handler: CallHandler) -> CallHandler
                                     (a decorator — it returns the handler)
     java      RelayClient.java:416  public void onCall(...)
   Corrected, kept.

C. SWMLBuilder.add_section / reset — THE GO TRAP FIRED AGAIN, AND THE NEGATIVE CONTROL CAUGHT IT.
   The rationale ("Python returns void / a tuple / a dynamic value") is false — reference
   core/swml_builder.py:235,266 return Self, and java returns SWMLBuilder, which is semantically
   the same fluent shape. I deleted both on that basis and THE GATE WENT RED:

     return-mismatch: returns 'class:Self' vs 'class:com.signalwire.sdk.swml.SWMLBuilder'

   The checker's fluent fold only fires when the reference is literally `void`; `class:Self` is not
   folded. Both entries restored with corrected wording. This is the same MISWORDED-BUT-REAL mode
   that reded go's nightly earlier today (85a2f72) — deleting a badly-explained but TRUE entry
   removes a real exclusion and breaks the gate.

D. AgentBase.skill_manager — THE ONLY GENUINELY FALSE ENTRY, DELETED.
     reference core/agent_base.py:308  self.skill_manager = SkillManager(self)   (plain attribute)
     java      AgentBase.java:2159     public SkillManager getSkillManager()
   The accessor-fold already reconciles these — both enumerate identically, so the omission
   excused nothing and was a pure blind spot. Gate stayed green after deletion.

E. VERIFIED TRUE, UNTOUCHED: unregister, SkillBase.register_tools/setup,
   get_basic_auth_credentials, add_answer_verb, enable_sip_routing,
   set_dynamic_config_callback, on_debug_event, on_summary, and RelayClient.receive/unreceive.
   That last pair is load-bearing: it proves the inversion is NOT a file-wide defect but is
   confined to the Action family plus the two on_call entries. Every correction was made per-entry
   against source, never by pattern.

Verification:
  baseline (clean tree): SURFACE exit 0 / 9 rules PASS · LEDGER exit 0 / 2 rules PASS
  after edits:           SURFACE PASS · LEDGER PASS · NO-LAUNDER PASS
                         (impossible: 34 · approved: 0 · idiom: 0 · unclassified: 0 · banned: 0)
  The finding-C red above is the load-bearing evidence, not the green: a deletion that should
  have been safe was caught by a real gate failure before shipping.
  run-format.sh exit 0, changed nothing beyond the ledger. NO java source touched.

FOR AN OWNER, NOT DONE HERE: the `class:Self` vs `class:<concrete>` mismatch in finding C looks
like an ORACLE/CHECKER gap rather than a java divergence. Extending the fluent fold to treat
`class:Self` as equivalent to any `class:` would make these entries genuinely deletable
fleet-wide — 7 `class:Self` occurrences in the oracle, and go carries the same pair. porting-sdk
was not touched.

PRE-EXISTING, NOT FROM THIS CHANGE: semver-diff reports a REPORT-ONLY MISMATCH on
strip_control_chars, present in the baseline and inherited from 392ecec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…he engine never reads

TWO defects at one call site in DatasphereServerlessSkill.getSwaigFunctions(), both silent.

1. THE SEARCH PAYLOAD WAS DROPPED ENTIRELY. The skill called
       .body(Map.of("query_string", "${args.query}", "document_id", documentId,
                    "count", count, "distance", distance))
   where the reference calls .params(webhook_params)
   (skills/datasphere_serverless/skill.py:211).

   THE ENGINE READS EXACTLY TWO PAYLOAD-BEARING FIELDS OFF A WEBHOOK OBJECT.
   mod_openai/actions.c:735-739 reads url, method, form_param, `params`, `headers`;
   `grep -n 'GetObjectItem(webhook, "' actions.c` returns ONLY params and headers. There is no
   read of "body" anywhere in that file. The outbound request is built from `params` (merged with
   args at :749-758 under input_args_as_params, posted as "params" at :2032).

   So query_string / document_id / count / distance never reached the server. This does NOT error
   — the search runs with no search parameters and returns whatever an unfiltered query gives back.

2. ${formatted_results} WAS NEVER POPULATED. The output interpolates it
   (DatasphereServerlessSkill.java:77) but nothing produced it: the chain went
   .webhook().body().output() with no .foreach(). The reference builds it at
   skills/datasphere_serverless/skill.py:212-224. DataMap.foreach already existed and worked
   (DataMap.java:198) — it was simply never called.

Both fixed to mirror the reference: .params(...) then .foreach({input_key: "chunks",
output_key: "formatted_results", max: count, append: "=== RESULT ===\n${this.text}\n" + 50x"=" }).

WHY NO GATE CAUGHT THIS, AND WHY THE NEW TEST IS SHAPED THE WAY IT IS. The existing 13 tests in
DatasphereSkillTest are CONSTRUCTION assertions — testServerlessSwaigFunctionsReturned checks that
a "data_map" key EXISTS and never looks inside it. Every one of them passed with both defects
present. SIGNATURES/DRIFT compare builder shapes, which were fine. Nothing in the fleet compares
an emitted DataMap payload against the reference.
The new test asserts on the EMITTED PAYLOAD: that no "body" key is present, that "params" carries
the four fields, and that a "foreach" block names chunks -> formatted_results.

Verification (matched pair, real gradle runs):
  BEFORE fix: testServerlessWebhookCarriesParamsAndForeach FAILED at DatasphereSkillTest.java:129
              — the red landed ON the new assertion (assertFalse containsKey("body")), not
              upstream of it.
  AFTER fix:  DatasphereSkillTest 14 tests, failures=0 errors=0 (from the JUnit XML report).
  Full suite: ./gradlew test BUILD SUCCESSFUL.
  scripts/run-format.sh applied formatting to these two files only; git status shows nothing else.

SIX OF NINE PORTS ALREADY DO THIS CORRECTLY — go pkg/skills/builtin/datasphere_serverless.go:148
("params" + foreach, an exact structural match to the reference), typescript, ruby, php, rust,
dotnet. java was alone in using body at this site.

STILL OPEN, NOT FIXED HERE (task #93): cpp src/skills/builtin/datasphere_serverless.cpp:43 and
perl lib/SignalWire/Skills/Builtin/DatasphereServerless.pm:44 also interpolate ${formatted_results}
without a foreach. perl is the bigger job — it bypasses the DataMap builder entirely and emits a
raw hash with no params, no body and no foreach, so its webhook has no request payload at all.

FOR AN OWNER, AND UPSTREAM OF ALL NINE PORTS (task #176): the REFERENCE itself ships both
core/data_map.py:260 `body()` -> writes a "body" key, and :276 `params()` -> writes "params",
with params()'s own docstring calling itself an "ALIAS FOR BODY". They are not aliases — different
keys, and only one is ever read by the server. body() is a builder method that silently discards
its argument. Per the spine (the server is the spec) the reference needs a ruling: make body()
write params, delete it, or at minimum stop documenting them as equivalent. A read-only lane is
classifying every other `body`-on-a-webhook call site across the fleet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
TLS-VERIFY was offered to java by porting-sdk and absent from the BEHAVIORAL suite's --rules list,
so it had never executed in this port. It is a SECURITY property and should not be optional per
port. (java already scheduled CA-VAR, SECURE-DEFAULT and SECRET-SCRUB; TLS-VERIFY was the one gap.)

BURNED TO ZERO BEFORE WIRING, per the standing rule: run standalone against the current tree
first — java TLS-VERIFY PASS. This adds coverage without adding a red.

TIER: BLOCKING (per-PR), measured not assumed — timed at 0s on this tree. Note this is a
suite-member addition, so it costs nothing beyond the BEHAVIORAL gate java already runs; it does
not add a gradle invocation.

The gate `desc` string is updated alongside the --rules list; a desc that enumerates rules it does
not schedule reads as coverage and is worse than no desc.

Verification — the full suite as CI will now run it, not the new rule in isolation:
    behavioral.py --port java --rules <the full 20-rule list from run-ci.sh>
    -> [BEHAVIORAL] all 20 rules PASS
  CA-VAR's own output confirms the neighbouring security rules inspect real source:
    "java: clean — REST reads SIGNALWIRE_REST_CA_FILE (.../rest/TlsContext.java:23),
     RELAY reads SIGNALWIRE_RELAY_CA_FILE (.../relay/RelayClient.java:767)."

Found by the fleet-wide gate-wiring audit (task #55); part of that item's tier-1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Documents all 108 undocumented public declarations in RelayEvent.java —
the base envelope accessors plus every typed event subclass's payload
getters — stating the wire key each reads, what the value means, and the
absent-value behaviour.

Wire semantics called out where the signature does not carry them:
  * CallDialEvent has no top-level call_id (tag correlates the outcome;
    the winning leg's id is nested at params.call).
  * CallDetectEvent reports its result at detect.params.event, not in a
    flat state field.
  * CallCollectEvent/CallPlayEvent share one control_id under
    play_and_collect, so a finished play is not a collected input.
  * CallReferEvent: the NOTIFY response code reports transfer outcome;
    the REFER response code only reports acceptance.
  * authorization_state is a re-auth credential, and inbound message
    bodies are untrusted end-user input.

Also repairs two pre-existing malformed Javadoc comments in QueueEvent
whose bare '<-' the javadoc tool parsed as an invalid HTML tag (2 of the
6 warnings 'gradlew javadoc' emitted before this change).

Docs only: no code, signature, or behaviour change.
DOC-SURFACE 59.5% (970/1631) -> 66.1% (1078/1631).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Documents all 79 undocumented public declarations in AgentBase.java —
the Builder and its setters, build(), the prompt/POM methods, the tool
registration overrides, the AI-config surface, the 5-phase verb methods,
skills, web/HTTP config, SIP routing, lifecycle callbacks, the getters,
and the DynamicConfigCallback interface.

Behaviour stated where the signature does not carry it:
  * build() resolution precedence (builder > config file > env > default)
    and its two warn-only security fallbacks: an auto-generated password
    that 401s every external caller, and an unset signing key that leaves
    webhook signature validation off.
  * setGlobalData / setPromptLlmParams / setPostPromptLlmParams MERGE
    despite the 'set' name, matching the reference — a replace would drop
    keys contributed by skills and earlier callers.
  * onFunctionCall does not validate a SWAIG token by design: the token
    is a wire artifact validated by the HTTP /swaig handler, not on this
    in-process dispatch path. It never throws.
  * setPromptText and promptAddSection flip POM mode in opposite
    directions, so the other one's content stops rendering.
  * registerSipUsername lower-cases, and REJECTS a username failing the
    character pattern with only a log warning.
  * promptAddSubsection / promptAddToSection silently no-op on an unknown
    parent title.
  * Security semantics on getAuthPassword, setPostPromptUrl,
    addSwaigQueryParams, enableDebugEvents, and DynamicConfigCallback's
    network-sourced inputs.

Also reattaches the SWAIG tool-authoring guidance (the 'descriptions are
prompt engineering' block): it sat above three '//' line comments, which
detached it from defineTool so the javadoc tool discarded it entirely.
Merged into defineTool's own comment.

FINDING (not fixed, docs-only change): enableDebugRoutes() is a no-op —
it returns this and sets nothing. Documented as accepted-for-parity.

Docs only: no code, signature, or behaviour change.
DOC-SURFACE 66.1% (1078/1631) -> 70.9% (1157/1631).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Documents all 51 undocumented public declarations in Service.java: the
auth getters, getDocument(), the RouteCollector HttpServer delegates, and
the 38 schema-driven SWML verb methods.

Each verb's summary is taken from the bundled SWML schema
(src/main/resources/schema.json) rather than paraphrased, so the surface
nine porting teams read matches what the server documents.

Distinctions the signature does not carry:
  * execute() returns to the caller; transfer() does not.
  * record() blocks SWML execution (voicemail); record_call() runs in the
    background and needs stop_record_call.
  * The 'prompt' VERB gathers input (digits by default, speech opt-in);
    the 'prompt' KEY inside ai() configures the agent. Different things.
  * ai()'s prompt argument must be an object -- a bare string is a fatal
    call error.
  * amazon_bedrock is its own verb, not an engine option on ai().
  * goto/return/switch are reserved words in Java, hence gotoLabel /
    returnVerb / switchVerb; the wire keys are unchanged.
  * goto cannot jump across sections.
  * sip_refer acceptance is not transfer completion.

Security semantics noted where the verb moves call content or money off
the platform: tap/live_transcribe/live_translate stream what was said to
a caller-supplied endpoint; pay keeps card data on the platform; send_sms
is billable per execution; user_event's payload reaches the browser
client. getAuthPassword documents that comparison must go through the
constant-time validateBasicAuth.

Docs only: no code, signature, or behaviour change.
DOC-SURFACE 70.9% (1157/1631) -> 74.1% (1208/1631).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Documents 75 undocumented public declarations across the three files:
Action.java (24) -- the base accessors, callback registration, and every
subclass sub-command; FunctionResult.java (26) -- the action-builder
methods; SWAIGFunction.java (25) -- accessors and the Builder.

Behaviour stated where the signature does not carry it:
  * setOnCompleted fires IMMEDIATELY when the action already resolved, so
    a callback registered after the terminal event lands is not dropped;
    it fires exactly once either way.
  * Each subclass stop() names the RELAY method it sends (verified
    against Constants.java, not inferred from the class name).
  * CollectAction.startInputTimers exists for collects dispatched with
    timers held off, so the caller is not timed out mid-prompt.
  * FunctionResult.setPostProcess only reaches the wire when true AND at
    least one action exists.
  * executeSwml accepts Map or JSON String, passes unparseable text
    through as raw_swml, and throws on any other type.
  * setMetadata is per-call scratch state the model does not see;
    setGlobalData is visible to the model and to DataMap expressions.
  * replaceInHistory is the lever for keeping sensitive tool data out of
    the transcript the model re-reads.
  * SWAIGFunction.isExternal is DERIVED -- true exactly when a webhookUrl
    was supplied -- not independently configurable.
  * secure defaults to true; opting out publishes a webhook anyone who
    can see the rendered SWML can invoke.
  * description and each parameter description are prompt text the model
    reasons over, not developer comments.

Docs only: no code, signature, or behaviour change.
DOC-SURFACE 74.1% (1208/1631) -> 78.7% (1283/1631).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Documents 119 undocumented public declarations across eight files:
Context (18), Step (13), SecurityConfig (14), Logger (16), Message (17),
RelayClient (17), Call (11), SwmlRenderer.RenderOptions (13).

Behaviour stated where the signature does not carry it:
  * Context.setConsolidate / setFullReset SUPPRESS the setIsolated
    history wipe -- configuring a reset wins over the wipe.
  * Context.setPrompt and addSection/addBullets are mutually exclusive
    and throw IllegalStateException on mixing; setEnterFillers ignores a
    null map rather than clearing.
  * Message.setState and Call.setState overwrite the field WITHOUT
    running the terminality/listener machinery an incoming event does, so
    waitForAnswered() and the completion callback do not observe them.
  * RelayClient.build fails PRE-CONNECT on a missing OR EMPTY credential,
    naming the variable; the JWT path needs neither project nor token;
    space always defaults so it is never itself the error.
  * RelayClient.onOpen: open is not connected -- isConnected() stays
    false until the handshake completes. onMessage swallows handler
    exceptions so one bad frame cannot kill the reader thread.
  * Logger strips ASCII control characters before output, so a logged
    value cannot forge log lines.
  * SecurityConfig getAllowedHosts and getCorsOrigins both default to
    ['*'] -- documented as the open default it is, with what that
    exposes, rather than left for a reader to discover.

FINDING (not fixed, docs-only change): Message.setOnCompleted does NOT
have the already-resolved guard that Action.setOnCompleted has. Message
.resolve() fires the callback inline, and setOnCompleted only stores it,
so a callback registered after the terminal event has landed never runs
-- the exact race Action.setOnCompleted was written to close. Documented
as the current behaviour ('register it before the message can settle');
worth an owner ruling on whether Message should match Action.

Docs only: no code, signature, or behaviour change.
DOC-SURFACE 78.7% (1283/1631) -> 86.0% (1402/1631).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
mjerris and others added 30 commits July 30, 2026 02:53
Nothing guarded `port_signatures.json` freshness — SURFACE-FRESH covers only
`port_surface.json`. That artifact is DRIFT's INPUT, so a stale one means the
parity gate compares against a fiction and passes.

Not a `_surface_commands.py` table entry: only 8 of 10 run-ci scripts read
that table, so a table entry would be silently skipped where it isn't read.

`res=gradle` because java's enumerator rebuilds the JAR and reads it, so this
must not overlap the other Gradle-touching gates nor the SURFACE suite's own
in-place regen-then-restore of the same artifacts.

Not vacuous on arrival: it caught a live staleness in this very branch when
the SWAIG token work briefly added a public method the committed artifact did
not carry, and reported the twelve differing leaves by path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Every generated wire-type DTO carried "no methods (the reference records this
as a method-less type definition)" in its class javadoc — 1,071 shipped public
doc comments explaining the SDK's own type in terms of the porting project's
reference, which means nothing to a developer reading the javadoc.

The FACT is worth keeping and is kept: these are pure data DTOs with public
snake-wire-key fields and no accessors. Only the framing goes, replaced with
what the reader actually needs — "read and write the fields directly".

ONE template edit covers all four generators: `generate_swml_verbs.py`,
`generate_relay_protocol.py` and `generate_swaig_payloads.py` all dynamically
load `generate_rest.py` and reuse its `emit_type_class`, so the wording is
shared rather than restated per generator.

Regenerated, not hand-edited: 828 REST + 158 SWML-verb + 123 RELAY-protocol +
20 SWAIG-payload files. GEN 5/5 rules PASS (byte-fresh + formatter-clean).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…om the CLI

`swaig-test --simulate-serverless --exec` could not invoke a `secure` tool —
the default — because it emitted no `__token` and no `call_id` at all. Once
token enforcement landed, every secure tool became unreachable from the CLI on
both serverless platforms.

The CLI drives the agent IN-PROCESS, so it can mint the credential from the
very SessionManager that will check it. It now does, on the same query string
the platform uses when it calls a rendered webhook back, with the `call_id` in
the body — a token can only be validated against a call id, so a request had
to carry one.

`SIMULATED_CALL_ID` is fixed rather than random: a CLI invocation has no live
call, and the only thing that matters is that the token and the id it is
checked against agree.

Also fixes three tests that were asserting the pre-enforcement behaviour:

  - `LambdaAgentHandlerTest` swaig routing/base64-decoding cases now carry a
    genuine token, so they test routing and decoding rather than being decided
    by the token check.
  - The two webhook-signature suites declare their `ping` tool `secure(false)`.
    They isolate the SIGNATURE layer; with a secure tool, the signed-vs-unsigned
    comparison would be decided by the token check instead of the signature
    check it exists to exercise.

2327 tests / 0 failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Shipped public javadoc described this SDK by comparison to the internal
porting reference — "Mirrors Python's `infer_schema`", "Like the Ruby port",
"Ported from the Python FAQBotAgent.search_faqs", "(agent_base.py:1040)",
"the Java analog of Rust's `#[non_exhaustive]`". A developer reading the
javadoc has no idea what "the reference" is, so these sentences spent their
words on something the reader cannot see.

749 occurrences across 96 hand-written files, now 0.

MOST CARRIED A REAL FACT, so most were rewritten rather than deleted —
deleting a sentence that states behaviour would be a regression, not a fix.
Cases where the framing was the ONLY statement of a behaviour:

  - `SessionManager` base64url padding — kept the WHY: a strict decoder
    rejects input whose `=` padding was stripped, so a token emitted without
    it is undecodable.
  - `SessionManager.validateToolToken` — the framing hid a real trap: its
    parameter order differs from `validateToken`'s and both are `String`, so
    a swap compiles and silently fails every validation. Now a warning.
  - `CallState`/`DialState`/`MessageState` — "the analog of `#[non_exhaustive]`"
    was the only statement of the open-enum contract; now says to handle the
    unknown-value case and why `fromWire` returns null instead of throwing.
  - `AbortSignal` — cancellation is cooperative, checked BETWEEN attempts; it
    never tears down an in-flight request.
  - `PaginatedIterator` — no request is issued until the first `next()`.
  - `SchemaUtils` — what "lightweight" actually means: verb existence plus
    required properties, NOT property types or nested constraints.
  - `Action.await`/`Message.await` — the name avoids `Object.wait()`, which is
    final; the parameter is SECONDS, and null means indefinite.

Self-language references stay — "Java erases lambda parameter names at
runtime" explains Java to Java developers. So do genuine protocol references
(SWML, SWAIG, mod_openai, wire keys) and the `mod_openai`/`schema.json`
citations explaining why the security param is `__token`.

Comments only: a comment-stripped diff of all 99 files against HEAD is empty,
so no signature, name, or behaviour changed. DOC-SURFACE stays at 100%
(1631/1631) — no file lost a `@param`/`@return`/`@throws` tag.

Two pre-existing doc inaccuracies corrected in passing: `RelayClient.getToken()`
and `getJwtToken()` return null on the unused auth path, not empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Owner ruling: "all the full directories should be linted and formatted
including tests examples and all ... at the levels the shipping code gets,
examples and tests are shipping code too". There is ONE bar — the one the
shipped library already meets — and everything now meets it.

Three carve-outs removed:

1. The `examples` source set had BOTH analyzers turned off outright:

       tasks.named('compileExamplesJava') { options.errorprone.enabled = false }
       tasks.matching { it.name == 'checkstyleExamples' }.configureEach { enabled = false }

   Its stated reason — "an unused import in a demo must not fail the SDK
   build" — is precisely what the reference refuses. signalwire-python's own
   comment on its examples/: "Everything else (F401 unused imports, F541 empty
   f-strings, C4/B/RET/SIM/...) is a real smell and stays enforced so
   `examples/**` is gate-cleanable to zero." A user copies an example
   verbatim, so a real bug in one is a real bug shipped. All three example
   trees (examples/, rest/examples/, relay/examples/, 73 files) are now under
   Error Prone warnings-as-errors + Checkstyle.

2. Spotless's target was `src/**/*.java`, so the same 73 example files plus
   scripts/SignatureDump.java had never been formatted at all. Widened to
   cover every Java source in the repo.

3. scripts/SignatureDump.java belonged to no Gradle source set — it is
   compiled by raw javac from scripts/enumerate_signatures.py — so no lint
   or format touched it. That is a carve-out by omission, and the file is
   load-bearing: it produces port_signatures.json, the input to the
   SIGNATURES and DRIFT gates. A new `auditTools` source set puts it under
   the same bar; nothing depends on its output.

Nothing is excluded now. There is no vendored third-party Java in this repo,
so there is nothing that legitimately could be.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…site

JdkObsolete was OFF for the entire codebase to accommodate exactly one test:
WebhookFilterTest's HttpServletRequest dynamic proxy must return a
StringBuffer from getRequestURL(), because that is the return type the servlet
interface declares and the JDK proxy would ClassCastException on a
StringBuilder.

The substance was right — the obsolete type is forced by the interface being
implemented, not chosen — but disabling the check repo-wide turned a per-site
fact into a repo-wide blind spot: any NEW Vector/Hashtable/StringBuffer
anywhere in the SDK was invisible to the gate.

Converted to a per-site @SuppressWarnings("JdkObsolete") on Handler#invoke
carrying that reason, and the check is ON everywhere else. Narrower than what
it replaces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
scripts/SignatureDump.java is the audit helper that reflects over the built
JAR to produce port_signatures.json — the input to the SIGNATURES and DRIFT
gates. It had never been linted (it belongs to no Gradle source set and is
compiled by raw javac), and the newly-added auditTools source set surfaced
4 Checkstyle errors.

Three were `catch (Throwable)` around reflection calls. The intent was right:
Class.forName / getDeclaredClasses / getMethod raise NoClassDefFoundError — a
LinkageError, not an Exception — when a class references a compile-only
optional dep (javax.servlet) absent from the dump classpath, and skipping
those is correct. But Throwable also swallows OutOfMemoryError,
StackOverflowError and ThreadDeath. In a tool whose contract is "enumerate
every public symbol", a swallowed JVM Error means a SHORT signature dump that
looks successful — and a short dump silently shrinks the surface the DRIFT
gate compares.

Narrowed all three to `catch (Exception | LinkageError)`, which covers every
case the code actually intends to handle and lets a real JVM failure
propagate. Each site carries the reason.

The fourth was EqualsAvoidNull: `n.equals("module-info.class")` -> literal on
the left.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Mechanical google-java-format pass over the example trees Spotless had never
seen, now that the FMT target covers the whole repo. (scripts/SignatureDump.java
was the 74th such file; its reformat rode along with its lint fix in bad84ac.)

Spotless's removeUnusedImports() also cleared the 7 unused imports Checkstyle
found in the example trees, which were the entire pre-existing Checkstyle
debt there:

    examples/KubernetesReadyAgent.java:16          java.util.List
    examples/SessionAndStateDemo.java:11           java.util.ArrayList
    examples/DataMapDemo.java:12                   java.util.List
    examples/RelayAuditHarness.java:35             java.util.Collections
    examples/McpAgent.java:23                      java.util.List
    rest/examples/RestFabricSwmlAndCallflows.java:17  java.util.List
    rest/examples/RestBindPhoneToSwmlWebhook.java:18  com.signalwire.sdk.rest.PhoneCallHandler

The last one was propping up a commented-out illustrative block, not live
code. No behavior changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Twelve example programs across eight files advertised a result and printed a
Java identity hash instead:

    System.out.println("  Available: " + numbers);   // AvailablePhoneNumbersResponse@4488aabb

Every generated REST response type is a pure-data DTO — public fields, no
toString() — so string concatenation yields `Type@<hash>`, never data. Anyone
running or copying these demos saw a hash where the SDK's entire point was the
result. The bug is invisible at compile time and survives any smoke test that
only checks the program does not crash.

Sites, and what each now prints:

    examples/RestDemo.java:40                          .data
    rest/examples/RestPhoneNumberManagement.java:31    .data
    rest/examples/RestManageResources.java:54          .data
    rest/examples/RestFabricConferencesAndRouting.java:39  .data
    rest/examples/RestFabricSubscribersAndSip.java:73  .data
    rest/examples/RestVideoRooms.java:56               .data
    rest/examples/RestQueuesMfaAndRecordings.java:43   .data
    rest/examples/Rest10dlcRegistration.java:49        .data
    rest/examples/RestDatasphereSearch.java:44         .chunks
    rest/examples/Rest10dlcRegistration.java:40        id/name/state
    rest/examples/Rest10dlcRegistration.java:58        id/name/state
    rest/examples/RestFabricSubscribersAndSip.java:64  id/username

Error Prone's ObjectToString found these, and it is promoted from its shipped
WARNING severity to ERROR in the same change — a WARNING does not fail the
LINT gate, so all twelve would have stayed. The promotion costs nothing beyond
these fixes: after them the repo is at zero, and the next DTO concatenated
into a log line or an example reds the gate instead of shipping.

RED-before / GREEN-after:
  before: compileExamplesJava -> 12x "error: [ObjectToString]", exit 1
  after:  compileExamplesJava -> 0 ObjectToString findings, exit 0

None of this was reachable until the examples/ lint carve-out came out. It was
never a style question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
This repo is a Java SDK, but scripts/ carries 8 hand-written Python files that
are entirely load-bearing: the enumerators that produce port_signatures.json
and port_surface.json (the inputs to the SIGNATURES, SURFACE and DRIFT gates)
and the generators that emit the committed REST/RELAY/SWML/SWAIG trees. No
gate linted or formatted any of them — the tooling that enforces the Java bar
was itself unchecked. No port in the fleet had a Python lint config.

eng/ruff.toml copies signalwire-python's [tool.ruff.lint] select verbatim, so the
fleet gets ONE Python bar rather than ten invented ones, and pins
`preview = false` for the same reason the reference does (an unpinned
formatter resolves preview rules in CI but not locally).

78 findings burned to zero. Nothing is excluded by directory. The only
per-file entries are S603/S607 across the five files that shell out: every one
of those call sites is list-form with the default shell=False, so there is no
shell to interpolate into, and every argv vector is fixed at the site with
only repo-computed paths or sys.executable varying. S607 fires on javac/java/
git resolved via PATH, which is how a build tool is supposed to invoke its own
toolchain. Same rationale and same per-file scoping as the reference's
cli/dokku.py entry, so a new subprocess call in any OTHER file still reds the
gate. One per-line `# noqa: E402` on enumerate_signatures.py's sibling import,
which is structurally forced to follow its sys.path insert.

The burn included several genuine authoring slips, all in generator code:
a duplicate "Thread" in JAVA_BUILTIN_COLLISION, a dead `head_start` local, an
unused `verb` unpack, and eight loop variables destructured but never read.

Wired into the existing FMT and LINT gates rather than adding new ones:
run-format.sh gains `ruff format` (apply locally, --check in CI, matching its
Java half) and run-lint.sh gains `ruff check`. sw_ruff in scripts/_env.sh
fails LOUD when ruff is absent so the Python half can never silently skip, and
ruff is declared as a CI dependency in test.yml, nightly.yml and publish.yml.

The config sits at eng/ruff.toml rather than the repo root because porting-sdk's
ROOT-HYGIENE gate has no allowlist entry for `ruff.toml` and reds on it as root
clutter — even though its ALLOWED_ROOT_EXACT set already carries every other
port's root linter config (.golangci.yml, .rubocop.yml, phpstan.neon,
clippy.toml, .clang-tidy, .perlcriticrc, eslint.config.mjs). ruff takes --config,
so moving the file is the honest fix here rather than an allowlist entry for
something that can simply move; sw_ruff passes --config exactly once so no call
site can forget it. Adding `ruff.toml` to that set upstream is the better fix and
is reported as a cross-repo finding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
README.md embeds three code blocks byte-identically from example fixtures
(examples/Quickstart{Agent,Relay,Rest}.java, anchored by `<!-- include: … -->`
markers and `region:` spans). Bringing the example trees under Spotless
reflowed those fixtures, so the README-INCLUDE gate correctly went red at all
three sites.

The fixture is canonical, so the README blocks follow it. No prose changed;
this is the same google-java-format reflow, propagated.

    [readme-include] java: clean (3 include site(s) verified).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
`--strict` was declared `action="store_true"` and documented in this
script's own usage header (line 20), but NO gate ever passed it — measured
across all ten run-ci.sh scripts, every one has zero `--strict` references,
as does porting-sdk's _signatures_fresh.py. So the fail-loud branch
(`if args.strict: return 1`) was unreachable: a type that failed to
translate silently DROPPED THE WHOLE SYMBOL, the artifact was written
anyway, and the process exited 0. The gate then compared this port against
an artifact missing that surface — and blamed the port for an omission it
never had.

Switched to argparse.BooleanOptionalAction with default=True, matching
rust and cpp (which already had it) and the landed reference fix in
porting-sdk 645d8ad. `--no-strict` remains as the explicit escape hatch.

This immediately surfaced 3 REAL failures that were rotting the artifact --
TlsContext.from_ca_file, TlsContext.socket_factory and
WebService.file_allowed[path]. Their types are added to
porting-sdk/type_aliases.yaml in the sibling commit; the regenerated
artifact here gains the 42 lines carrying exactly those three symbols.

Verified:
  negative control  emptied the java alias table -> exit 1, artifact NOT
                    written (checked by sha256, not `git diff`)
  positive control  clean regen -> exit 0
  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
…s recorded a shorter surface than Java actually offers

Java has no default arguments; its idiomatic substitute is a shorter overload
that omits the trailing params and forwards the reference's defaults —
`waitForUser()` delegating to `waitForUser(enabled, timeout, answerFirst)` IS
`enabled=..., timeout=..., answer_first=...`. The enumerator's overload collapse
preferred the FEWEST-param overload, so the recorded signature was the
convenience form and every parameter only the full form carries vanished from
the artifact:

    FunctionResult.wait_for_user   recorded (self)                  oracle (self, enabled, timeout, answer_first)
    PomBuilder.add_section         recorded (self, title)           oracle (self, title, body, bullets, numbered, numbered_bullets)
    Context.add_step               recorded (self, name)            oracle (self, name, task, bullets, criteria, functions, valid_steps)

Those read as `param-count-mismatch` and were excused one method at a time, so
the port looked like it had dropped capability it in fact ships. A hand-
maintained PREFER_FULL_OVERLOAD set was already un-collapsing 20-odd of these
individually; this makes the rule structural instead of itemised.

`optional_param_names` already detects the shape — it marks a param optional
when a shorter overload omits it, requiring the shorter one's names to be a
PREFIX of the full one's. The collapse now consults the same predicate: when
the overload set is a trailing-prefix delegate chain, keep the FULL-arity form,
which carries every reference param with the omitted ones already marked
`required: false`.

THE NARROWING IS LOAD-BEARING. Every shorter overload must be a prefix of the
full one, and the arities must be distinct — not merely one of them qualifying.
A set that MIXES a delegate with a same-arity type-swap sibling is not a
defaults chain, and preferring its full-arity member picks arbitrarily:
`SessionManager` offers `(int defaultExpiry, String secretKey)` beside
`(byte[] secretKey, int defaultExpiry)`, whose params are REORDERED and RETYPED
rather than extended. The first cut of this fold picked the byte[] form and
manufactured `secret_key: optional<string> vs list<int>` — a mismatch pointing
the wrong way. Requiring whole-set prefix consistency keeps the fewest-param
default there, so a genuine surface collapse still reports.

Measured as SETS, `--omissions` on both sides:

    excused  6335 -> 6290   (71 removed, 26 added)
    drift       0 ->    0   (0 added)

The 26 additions are not regressions — they are divergences that were
INVISIBLE while the short overload hid the params they live on, and they are
covered by the file's existing rationales. Comparison strictly widened with
them: params compared went 1033 -> 1138 (member) and 202 -> 241
(construction), with drift flat. 25 PORT_SIGNATURE_OMISSIONS entries the fold
made unnecessary are deleted rather than re-excused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…thods read as return drift for spelling "the receiver" differently

The reference annotates "returns the receiver" with the `Self` type-var, and the
oracle records it literally:

    python   def say(self, ...) -> Self:          returns: class:Self
    java     SWMLBuilder say(...) { return this; } returns: class:signalwire.core.swml_builder.SWMLBuilder

Java has no `Self` type — a fluent method can only name its own concrete class —
so the SAME contract compared unequal and 7 methods were excused as
`return-mismatch`. A caller chains identically either way; nothing about the
wire or the call differs. The enumerator now canonicalizes onto the oracle's
spelling, which keeps the return type COMPARED rather than excused.

THE FOLD IS SCOPED, in both directions, because an unscoped one would hide real
differences. It applies only when BOTH hold:

  * the class is one the reference actually declares `Self` on. Verified against
    the oracle, not assumed: `class:Self` appears as a return type exactly 7
    times in python_signatures.json, all 7 on SWMLBuilder — matching the 7
    excused entries precisely. Rewriting every self-returning Java method would
    erase a genuine difference wherever the reference returns the CONCRETE class.
  * the Java return already IS the declaring class. Rewriting a non-self return
    on a listed class would invent a fluent contract the method does not have.

Measured as SETS, `--omissions` on both sides:

    excused  6290 -> 6283   (7 removed, 0 added)
    drift       0 ->    0   (0 added)

The 5 PORT_SIGNATURE_OMISSIONS entries the fold made unnecessary are deleted
rather than re-excused; the other 2 were never separately listed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…ed MEASUREMENT, not erosion

The trailing-delegate overload union stopped collapsing overloads to their
fewest-param form, so 44 methods whose recorded arity previously differed from
the reference became measurable at all (unmeasured 226 -> 182). Two of them were
ALREADY-ERODED slots that the short overload had been hiding:

  FunctionResult.execute_swml(swml_content) — already `any` in the PREVIOUS
      committed port_signatures.json. It was skipped only because the port
      recorded arity 1 against the reference's 2, and the checker cannot align
      slots across a different arity. Not a new erosion, a newly VISIBLE one.

  Context.add_step(functions) — the `Object functions` param has been in
      Context.java all along on the 6-arg overload; the 1-arg convenience form
      the collapse used to pick simply did not carry it.

Verified rather than asserted: re-running the checker against the baseline
artifact in isolation gives 12 eroded / 226 unmeasured, and against the current
one 14 / 182. Neither slot's TYPE changed in this branch — only whether the gate
could see it. Widening what a gate can see must not read as the port getting
worse, and the alternative (keeping the collapse) buys a smaller number by
measuring less, which is the worse trade.

Context.add_step is the same accepted class as the three `union<string,
list<string>>` slots already inside the ratchet (FunctionResult.remove_metadata
/ remove_global_data / Step.set_functions): Java has no union type, so a
whitelist that is "a List<String> or the string "none"" can only be `Object`.

The bound still binds — at --max 13 the gate reports "REGRESSED by 1" and exits
non-zero, so this is a moved ratchet, not a disabled one.

ITEMISED for a porting-sdk decision, deliberately NOT worked around here:
diff_port_type_erosion counts a slot eroded when the port says `any` and the
reference does not, via `_VACUOUS = {"any", "", None}`. A union that CONTAINS
`any` is equally vacuous — it admits every value — but is not in that set. The
reference declares execute_swml's param `str | dict[str, Any] | Any`, so Java's
`Object` is its exact equivalent and arguably should not count at all. Fixing
that belongs in the shared checker, not in a port.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…ice — a stale premise left 10 duplicate port-only symbols

Java flattens Python's composed mixins / ToolRegistry onto its Service base
class. Both enumerators already PROJECT those methods to their canonical mixin
home, but neither removed the Service copy — so each method was emitted twice
and the duplicate needed a PORT_ADDITIONS excuse to stay green.

The surface enumerator stated the premise explicitly:

    Inherited methods are ADDED to the mixin path but NOT removed from
    SWMLService (they legitimately belong to both — the reference records
    them on both).

That is FALSE, and measured so. Of the nine mixin methods Java declares on
Service — define_tool, define_tools, on_function_call, register_swaig_function,
has_function, get_function, remove_function, validate_basic_auth,
on_swml_request — the surface oracle records ZERO on SWMLService. They live on
signalwire.core.mixins.* and signalwire.core.agent.tools.registry.ToolRegistry,
which is precisely where MIXIN_PROJECTIONS already files them.

The signature enumerator had the same hole from the other direction: it built
its projection source from {**svc_methods, **ab_methods} but its strip loop was
commented "Drop projected methods from AgentBase only" — so anything sourced
from SWMLService was projected and then left behind.

Both strips are ORACLE-KEYED, never a hardcoded list: a method the reference
genuinely DOES record on SWMLService (get_basic_auth_credentials, serve,
on_request, handle_request …) stays put, so this cannot hide a real member.

Folding made six PORT_ADDITIONS entries dead; they are deleted. Their rationale
was the self-refuting boilerplate ("idiomatic Java surface extension (builder,
getter/setter, overload …) with no Python-reference counterpart") — and the fold
disproves it directly: these are real reference capabilities that were merely
filed on the wrong class.

Set deltas, all four ledger files passed on both sides:

  signatures  drift 0 -> 0        OPENED=0  CLOSED=0
              excused 6238 -> 6228 OPENED=0  CLOSED=10
  surface     drift clean -> clean
              excused_additions 493 -> 487, dead_additions 0 -> 0

Excused did not rise to absorb drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…e — enabled-but-uncertified serves PLAINTEXT

TlsServerHttpsTest proves only the POSITIVE path (valid cert/key -> real
HttpsServer -> CA-verified session). Nothing covered the case where TLS is
switched ON and the cert/key is absent — the "not configured folds into TLS
off" shape (#90). This adds that quadrant.

WHAT THE PROBE FOUND

AgentServer.resolveTls() returns null — "serve plaintext" — on THREE separate
not-configured conditions:

    if (cert == null || key == null)        return null;
    if (!Files.exists(Path.of(cert)))       { log.warn(...); return null; }
    if (!Files.exists(Path.of(key)))        { log.warn(...); return null; }

run() then falls through to HttpServer.create(). So SWML_SSL_ENABLED=true with a
missing cert yields a WORKING PLAINTEXT LISTENER — no exception, only a warn.
Measured, not read: tlsEnabledWithMissingCertServesPlaintext() calls
enableTls() with nonexistent paths, starts the server, and gets a plaintext 200
from /health with an EMPTY sslSession().

The second path is worse. Service.serve() — the SWMLService/AgentBase listener,
Service.java:1747 — calls HttpServer.create() UNCONDITIONALLY. It has no TLS
branch at all, so it cannot serve HTTPS regardless of configuration. This is the
same two-independent-paths shape go's lane hit, where agent.AgentBase built its
own server and did not inherit the swml-side refusal.

SecurityConfig already has the correct check — validateSslConfig() returns an
error for enabled-but-uncertified — but NOTHING on either serve path calls it.
Its only caller is getSslContextKwargs(), which no server consults. Meanwhile
getUrlScheme() returns "https" whenever sslEnabled, so a misconfigured service
ADVERTISES https:// webhook URLs while listening in cleartext.

WHY THIS COMMIT PINS RATHER THAN FIXES

The Python reference has the SAME fall-through (swml_service.py:1328 —
validate_ssl_config fails -> log warning -> self.ssl_enabled = False; web_mixin
then requires ssl_enabled AND cert AND key before passing ssl_certfile to
uvicorn, else plain uvicorn.run). Java therefore MATCHES the reference here.
Changing java to refuse would be a deliberate reference divergence and is an
owner decision, not a port fix, so the behaviour is pinned instead of altered —
a future change to it will now be visible and intentional.

WIRE-LEVEL METHOD

A grep is not a probe, so the contract is captured from raw opening bytes:

  httpsClientOpensWithClientHello  a raw ServerSocket that WOULD answer a valid
                                   200 to cleartext; a TLS client must instead
                                   send 0x16 (ClientHello) and fail. Observed
                                   on the wire: 16 03 03 01 a1 01 ...
  plainHttpStillWorks              SCOPE CONTROL — the same listener answers 200
                                   to an http:// GET, first byte 'G'. The
                                   refusal above is specific to TLS, not a
                                   listener that refuses everything.

SSLSocket is used rather than java.net.http.HttpClient deliberately and by
measurement: HttpClient does NOT emit a ClientHello against a silent peer (it
reports HttpConnectTimeoutException and the raw listener reads EOF, byte -1), so
it cannot witness the opening bytes. SSLSocket.startHandshake() is the layer
where the handshake is actually spoken.

NEGATIVE CONTROLS (both run, both went red as required)

  expected byte 0x16 -> 0x47   httpsClientOpensWithClientHello FAILED
  assertFalse -> assertTrue    tlsEnabledWithMissingCertServesPlaintext FAILED
  restored, 4 tests completed, BUILD SUCCESSFUL

ALSO CHECKED, CLEAN

  * Certificate verification is reachable and ON by default. The single
    trust-all site (McpGatewaySkill.InsecureTrustManager) is constructed only
    inside the if (!verifySsl) guard, and verifySsl defaults true at BOTH the
    field (line 55) and the param parse (line 170). No HostnameVerifier
    returning true anywhere in src/main/java.
  * SIGNALWIRE_REST_CA_FILE and SIGNALWIRE_RELAY_CA_FILE both work — each builds
    an SSLContext trusting ONLY that bundle (HttpClient.java:154,
    RelayClient.java:860, via TlsContext), not a trust-all shortcut.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…bclasses

Re-drift against porting-sdk oracle 0e0f935. Java's surface enumerator claimed 11
public members the reference no longer exposes, reddening SURFACE on this PR.

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). Sixteen skills now override the
protected hook, which the surface oracle does not record, so the public member
lives on SkillBase ALONE — recorded on the base and on ZERO of the 17 skill
subclasses.

Java has no protected-hook twin: its SkillBase is an INTERFACE whose
`default getPromptSections()` IS the override point, and the skip_prompt guard the
reference put in the template method lives in Java's CALLER instead
(SkillManager.java:116-118). The WIRE behaviour therefore matches — a skill with
skip_prompt=true contributes no prompt sections in either SDK. What did not match
was the SURFACE: each of the 13 Java overrides was emitted as public surface, so
11 became phantom missing-reference additions and 2 sat behind stale
PORT_ADDITIONS excuses.

THE FIX IS ORACLE-DERIVED, NOT A HAND LIST. `_base_only_skill_hooks` computes, at
call time, the members the oracle records on SkillBase and on no skills.* subclass
— exactly the final-template-method shape. Those are stripped from the concrete
skill classes. It cannot hide a real member: a hook the reference records on ANY
subclass (setup, register_tools, get_hints, get_global_data, get_parameter_schema,
get_instance_key, cleanup, __init__) never enters the base-only set, and
SkillBase's own copy is out of scope. A future reference move self-corrects on the
next regen with no code edit. Fail-safe: an unresolvable oracle yields an EMPTY
hook set and strips nothing, rather than emptying every skill class.

DEAD ENTRIES DELETED (required, not optional):
  PORT_ADDITIONS.md  ClaudeSkillsSkill.get_prompt_sections
  PORT_ADDITIONS.md  InfoGathererSkill.get_prompt_sections
Both stopped being emitted, so they excused nothing. Their rationale was the
self-refuting boilerplate ("idiomatic Java surface extension ... with no
Python-reference counterpart"); the fold disproves it — the capability is real and
the reference simply files it on the base.

Measured, --omissions + --surface-omissions + --surface-additions on both sides:
  surface   drift 11 -> 0        OPENED=0  CLOSED=11
            excused_additions 549 -> 547   FELL by the 2 deletions; absorbed nothing
            dead_additions 2 -> 0
  signature drift 0 -> 0         OPENED=0  CLOSED=0
            excused 6248 -> 6248 unchanged
  PORT_ADDITIONS 524 -> 522 (two DELETIONS; zero insertions)
  PORT_OMISSIONS / PORT_SIGNATURE_OMISSIONS  34 / 240  unchanged

Negative controls, all four arms:
  - Oracle-driven, not list-driven: adding get_prompt_sections to JokeSkill in a
    temp oracle copy stops it being stripped, with NO enumerator edit.
  - The converse: removing get_hints from every skill subclass in a temp oracle
    makes it base-only and it IS stripped, again with no edit.
  - Fail-safe: an empty oracle strips nothing (never an empty class surface).
  - Genuine divergence still surfaces: renaming JokeSkill::getHints to
    getHintsPerturbed in the real .java re-reds SURFACE with exactly one missing
    and one extra symbol while the other 16 skills stay folded. Reverted;
    re-verified green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
… surface

java was the ONLY port failing Doc Audit. Root cause: the native surface
enumerator never emitted the implicit accessors a compact Java ``record`` header
declares, so 30 real public methods across the SDK's 10 public records were
invisible to DOC-AUDIT — which resolves every identifier a doc snippet writes
against exactly that file.

WHY IT SURFACED NOW, AND ONLY IN CI. The workflow regenerates
port_surface_native.json before auditing (doc-audit.yml:113-121), so CI audits the
FRESH surface while a bare local run reads the committed one. The committed file
was stale — generated at c485c73, before fe5eef9 removed ``DataMap.body()``. That
stale copy still listed ``body``, so the name resolved locally and the gap stayed
hidden. Once fe5eef9 deleted the SDK's only other ``body`` member, the fresh
surface had no holder for it at all and the six doc references went unresolved:

    docs/agent_guide.md:2352            HttpResponse.body()   (JDK)
    docs/cloud_functions_guide.md:79    ServerlessAdapter.Response.body()
    docs/cloud_functions_guide.md:201   ServerlessAdapter.Response.body() x2
    docs/datamap_guide.md:186           HttpResponse.body()

The SignalWire ones are genuine: ``ServerlessAdapter.Response`` and
``Service.HttpResult`` are records declaring ``(int status, Map<String,String>
headers, String body)``, so ``body()`` is real, live, public API. The docs were
right and the enumerator was blind. ``status`` and ``headers`` were equally
unemitted but never flagged, because those bare names happen to resolve against
other classes — ``body`` was simply the one whose last other holder went away.

THE FIX IS NATIVE-ONLY, DELIBERATELY. ``record_component_accessors`` parses the
compact header's component list (depth-tracked, so ``Map<String,String> headers``
yields one accessor, not two; generic record headers step over their type
parameters) and the walker attaches them at both the nested and top-level record
sites. The native surface answers "does this identifier exist in the Java API",
which is the question DOC-AUDIT asks, and these accessors demonstrably do. The
PARITY surface is oracle-keyed and already carries the components the reference
records (BasicCredentials/BearerCredentials arrive via the oracle field-accessor
path), so emitting them there would claim members the reference does not expose.
Verified: the parity port_surface.json is byte-identical modulo provenance, and
SURFACE stays 9/9.

Emitted (4 surfaced records; the other 6 are idiom-scaffolding classes the surface
filter already drops, unchanged by this commit):
    ServerlessAdapterResponse   body, headers, status
    ServiceHttpResult           body, headers, status
    BasicCredentials            password, username
    BearerCredentials           credentials, scheme

port_surface_native.json is regenerated in the same commit; it had been stale
since c485c73 (it also gains the two AuthHandler credential records and drops the
removed DataMap.body).

Non-vacuity, both directions:
  - Stripping the record accessors back out of the FRESH native surface
    reproduces CI's failure exactly — ``1 unresolved symbol name(s): body
    (6 hit(s))``, the same six doc lines.
  - With them, DOC-AUDIT passes on the fresh surface (6056 resolved / 7287 total).

Verified: DOC-TRUTH 8/8 PASS, SURFACE 9/9 PASS, DOC-SURFACE 100.0% (1631/1631).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
Two gate-measurement fixes, no source changes.

1. -Xmaxwarns 100000. javac caps diagnostic output at 100 warnings per
   compilation by default, so the examples source set printed exactly
   "100 warnings" while its true count was 161. The LINT gate was
   UNDER-REPORTING: a burndown driven off the log would have looked
   finished with 61 findings still in the tree. The whole-tree total was
   212, not the 100 the CI log showed.

2. DefaultPackage OFF. The 73 example mains and scripts/SignatureDump are
   default-package BY CONTRACT, in three places that break together if a
   package is added:
     - examples/README.md:112 + docs/agent_guide.md document
       `java -cp "$CP:examples" SimpleAgent` -- the simple name IS the main
       class, and a package would force a matching directory layout on
       anyone copying the single-file example.
     - build.gradle's own runExample -PexampleClass=<ClassName> resolves
       mainClass from that bare simple name.
     - porting-sdk/scripts/examples_run.py (the EXAMPLES-RUN gate, in a
       DIFFERENT repo) hardcodes -PexampleClass={f.stem}.
   scripts/SignatureDump.java is the same: enumerate_signatures.py runs it
   as `java -cp <cp> SignatureDump` to produce port_signatures.json, the
   input to the SIGNATURES and DRIFT gates.

   This is a deliberate structural choice, not undone work, so the check is
   OFF with its rationale next to the existing IterableAndIterator and
   AnnotateFormatMethod entries -- not suppressed at 74 sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
HidingField found a real defect, not a style nit. AgentBase re-declared
`private String proxyUrlBase`, shadowing the `protected` field it inherits
from Service. The two manualSetProxyUrl implementations then wrote DIFFERENT
fields: Service's wrote Service.proxyUrlBase, AgentBase's override wrote its
own copy, and AgentBase read only its own. Nothing anywhere read Service's,
so on any AgentBase instance the superclass field was write-only dead state.
Removing the shadow leaves one field with one writer and one reader; the
AgentBase read/write behaviour is unchanged.

The 28 @OverRide annotations are annotation-only (24 RelayEvent.getCallId
overrides, 4 AgentBase accessors, 2 BedrockAgent setters); each was verified
to be a genuine override with an identical signature rather than an
accidental hide. No signature, body, or behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
StringSplitter (9 sites). Every site keeps its EXACT current behaviour:
split(regex, 0) is defined to be identical to the single-arg split(), so
trailing empty segments are still dropped -- the two-arg overload just
states that choice instead of leaving it surprising. This matters at two
sites where the last element is load-bearing and split(regex, -1) would
have been a real behaviour change:
  - ConfigLoader.setNestedKey indexes keys[len-1] as the key to WRITE, so a
    "SWML_FOO_" env var must still target "foo" and not "".
  - AgentServer's SIP username derivation registers parts[len-1], so route
    "/a/b/" must still derive from "b".
Guava's Splitter (Error Prone's own suggestion) was not used: Guava is not
a dependency, and adding a runtime dep to the shipped SDK to satisfy a lint
check would cost every user more than the check is worth.

AddressSelection (4 sites). The mock relays bound
new InetSocketAddress("127.0.0.1", port), which runs the literal through
name resolution and keeps only the first address. InetAddress.getLoopbackAddress()
is the JDK's canonical loopback with no resolver step -- same bind target.

UnusedVariable (3 sites), all genuinely dead:
  - SchemaUtils.validateWidened's `depth`. Checked before removing that it
    was not a dropped recursion guard: unlike validateAgainst, this method
    never recurses (it only calls the non-recursive checkType), so there is
    no depth to thread.
  - WireRelayDump.pushDialAnswered's `socket` -- push() broadcasts to the
    connection the mock already holds.
  - WebService's unused private Logger field (and its now-unused import).
Both methods are private with in-file call sites, so no public surface moves.

Also: UnnecessaryLongToIntConversion (an (int) cast narrowing WINDOW_MS on
the way to a long parameter), UnnecessaryParentheses, and UnnecessaryLambda
(a private factory returning a lambda, now the BiFunction method itself
bound by this:: at its two registration sites).

port_signatures.json and port_surface.json regenerate byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
DefaultCharset flagged captured.toString() in the two log-scrub tests. The
naive fix -- decoding as UTF-8 -- would have introduced a mismatch: the
capture stream was `new PrintStream(captured, true)`, which ENCODES with the
platform default charset, so decoding as UTF-8 only agrees with it on a
UTF-8 platform. Both tests assert on non-ASCII control characters (NUL, ESC,
BEL), which is exactly where an encode/decode charset mismatch bites.

Both ends are now pinned to UTF-8, so the round-trip is explicit and
platform-independent rather than accidentally agreeing on the CI runner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
NotJavadoc (73 files, every example tree + scripts/SignatureDump). Each
file's header block opened with `/**` while sitting BEFORE the imports in a
package-less file, so it attached to no declaration and was never Javadoc --
just a block comment wearing a Javadoc marker. Changed to `/*`. One line per
file, no code touched.

StringCaseLocaleUsage (9 sites) -> Locale.ROOT, not Locale.getDefault().
Every flagged site normalizes an ASCII PROTOCOL token that is then compared
against ASCII literals: HTTP query-param values switched on ("es", "true",
tier/industry names), an HTTP method name, switch keywords, and the DataMap
"lc:" template expansion. None is user-facing display text. Under a Turkish
default locale getDefault() maps 'I' to a dotless 'i' and every one of those
literal comparisons would silently stop matching -- the classic dotted-I bug.

JavaTimeDefaultTimeZone (5 sites) -> ZoneId.systemDefault(). All five are
"what time is it" demo tools whose POINT is the machine's local wall-clock
time, which is precisely what the bare now() already resolved to. This states
the dependency without changing behaviour; a fixed zone would be wrong here,
making the demo report someone else's clock.

EmptyCatch (1 site): the swallowed IOException is the 500-response path
failing because the client already disconnected; documented in place, since
there is no longer any channel to report it on.

ruff B007: an unused `cls` loop variable in scripts/enumerate_surface.py
renamed to `_cls`. This was the finding that actually made the LINT gate exit
non-zero -- see the summary in the final commit's verification.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
…nclude

Both follow from the explicit-time-zone change in the previous commit.

DOC_AUDIT_IGNORE.md: the doc auditor resolves every identifier appearing in
docs/ and examples/ against the SDK surface, so the newly-introduced
ZoneId.systemDefault() read as an unresolved symbol at 5 example sites. This
file is the JDK-symbol registry, not a gate-silencing allowlist -- it already
carries DateTimeFormatter#ofPattern, Duration#between, String#toLowerCase and
the rest of the standard library. ZoneId#systemDefault is the same category
and is filed next to its java.time sibling.

README.md: the AI Agents quickstart block is INCLUDED byte-for-byte from
examples/QuickstartAgent.java#quickstart, so changing the fixture required
re-syncing the doc. Re-derived mechanically from the fixture region rather
than hand-edited; README-INCLUDE now verifies clean across all 3 include
sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSHZoWoxoPVK6FuNaNKWFh
GEN-FRESH-SWAIG was red on 6 generated files. Two spec re-vendors landed in
porting-sdk without any port being regenerated; the generators did not change.

  * 4336b98 (2026-08-01) re-vendored post-prompt.yaml. It moved
    PostPromptSystemLogEntry's `context` / `step` / `step_index` from top-level
    properties into `metadata.properties`, citing tl_stamp_location (timeline.c)
    as where the server actually stamps them -- so those three top-level fields
    were never on the wire at that level, and they are dropped. The same
    re-vendor typed two PostPromptSwaigLogEntry fields off their call sites:
    `mcp_response` is the MCP tool's raw result text (actions.c:2158, "Not parsed
    JSON") so Map<String,Object> -> String, and `mcp_error` is a boolean const
    true present only when the tool returned no result (actions.c:2162) so
    String -> Boolean. Both committed types were wrong.

  * 99fd429 (2026-08-03) re-vendored swaig-response.yaml at mod_openai cac4984,
    which replaced the untyped `{}` property stubs with real types read off
    process_action's call sites: context_switch system_prompt/user_prompt ->
    String, hold timeout integer -> ["number","string"] (Long -> Double),
    playback_bg file -> String, transfer dest -> String. Its extractor also emits
    each action object's property keys ALPHABETICALLY, hence ContextSwitchAction's
    field order.

Net effect on the surface is a type tightening plus the three dropped fields;
DRIFT stays clean against the Python oracle. port_signatures.json drops the
three synthesized sidecar accessors to match. port_surface.json /
port_surface_native.json need no change (both verified fresh).

HoldAction.timeout Long -> Double is source-incompatible for a caller, but the
DTO is method-less and has no hand-written consumer; the suite compiles and
passes unchanged.

Verification (bash scripts/run-ci.sh -- only SIGNATURES-FRESH red, and only
because it compares `git show HEAD:port_signatures.json`, unsatisfiable from a
working tree; this commit is what satisfies it):
  [GEN:GEN-FRESH-SWAIG] ... PASS        (GEN suite: all 5 rules PASS)
  [SURFACE:DRIFT] ... PASS              (SURFACE suite: all 9 rules PASS)
  ./gradlew test --rerun-tasks: BUILD SUCCESSFUL, 5 executed
      208 classes, 2331 tests, 0 failures, 0 errors, 0 skipped
  run-format.sh --check: spotlessCheck clean; 8 python files already formatted

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016i9TibArqEmDszG7VYEGYg
…radle exited 0

`gradle test` reports BUILD SUCCESSFUL for three different outcomes, and only one
of them ran a test: the task EXECUTED, the task was UP-TO-DATE, or the task came
FROM-CACHE. Exit status cannot tell them apart, so the TEST gate could report
PASS having executed nothing.

Measured during the java SWAIG regen: `BUILD SUCCESSFUL in 6s / 1 executed`, no
build/test-results tree, and a re-run saying `1 from cache`. That regen changed
GENERATED types (HoldAction.timeout Long->Double, PostPromptSystemLogEntry.action
String->Object), so a replayed green was precisely zero evidence that the
regenerated types compile and pass. The real numbers (208 classes, 2331 tests, 0
failures) only appeared under a forced re-run.

The fix is an EXECUTION RECEIPT written by the test task's doLast. Gradle skips a
task's action list wholesale on UP-TO-DATE/FROM-CACHE and runs it exactly when the
task really executed, so the receipt's presence is a first-class execution signal
from the build tool itself. It lives outside build/test-results/ and is
deliberately NOT declared as a task output, so the cache can neither store nor
restore it. An afterTest listener (which likewise only fires on real execution)
puts the executed/failed/skipped counts in the receipt, so the gate prints what
actually ran instead of trusting a summary line.

Two alternatives were tested and rejected:
  * Asserting a fresh build/test-results/ tree DOES NOT WORK — FROM-CACHE
    RESTORES that tree, files and fresh mtimes and all, because it is a task
    OUTPUT and reproducing it is the cache's whole job. Verified by deleting the
    tree and re-running: `:test FROM-CACHE`, all 208 XML files back, nothing run.
  * Grepping `> Task :test UP-TO-DATE` out of the log makes gate correctness
    depend on Gradle's console formatting, which is presentation, not contract.

Gradle's caching is left ON. Reuse across the many gradle invocations run-ci makes
is a legitimate speedup; the defect was never that the cache exists, it was a gate
treating a cache hit as evidence of execution.

scripts/run-tests.sh also gains a gradle-flag pass-through (any argument starting
with `-`), because it previously hardcoded its invocation with no "$@" — so
forcing a real run meant bypassing the port's own entry point. The pass-through
alone would NOT have been sufficient: the default invocation would still return a
cached green.

Tradeoff, recorded in the script header: the assertion itself costs nothing (one
rm -f, one [ -f ]) and forces no flag, but a second consecutive run over an
unchanged tree now exits 1 instead of printing a hollow PASS.

Verification (exit codes read unpiped):
  * UP-TO-DATE negative control: `:test UP-TO-DATE`, BUILD SUCCESSFUL in 584ms,
    gate exit 1.
  * FROM-CACHE negative control: `:test FROM-CACHE`, BUILD SUCCESSFUL in 555ms,
    gate exit 1, and the XML tree restored while nothing executed.
  * Positive: `run-tests.sh --rerun-tasks` -> 5 tasks executed, gate exit 0,
    receipt executed=2331 failed=0 skipped=0. Corroborated independently of the
    receipt by summing the XML tree: 208 files, tests=2331, failures=0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016i9TibArqEmDszG7VYEGYg
The Java toolchain here is already fully pinned — spotless 6.25.0,
googleJavaFormat 1.22.0, checkstyle 10.17.0, errorprone 2.27.1 — but the PYTHON
half of the same FMT/LINT gates installed ruff UNBOUNDED in all three workflows,
so it was the one tool that could change under the repo without a commit:

  .github/workflows/test.yml:80     pip install ruff  -> ruff==0.15.21
  .github/workflows/nightly.yml:129 pip install ruff  -> ruff==0.15.21
  .github/workflows/publish.yml:56  pip install ruff  -> ruff==0.15.21

That is a green-locally/red-in-CI generator: CI resolves the newest release at run
time while a contributor runs whatever they installed months ago, so a ruff release
that adds a rule or changes a format heuristic reds FMT/LINT on code that never
changed. 0.15.21 is the fleet-wide ruff (signalwire-python/perl/php/typescript pin
exactly that).

Each install step now ASSERTS the resolved version afterwards, mirroring the
uncrustify pin in mod_infrastructure: pip can satisfy a spec from an unexpected
index or cache, and a pin that silently did not take is worse than no pin.

The local half is pinned too, so this is a real local==CI guarantee rather than a
CI-side hope: scripts/_env.sh declares SW_RUFF_VERSION and sw_ruff() fails loud
when the ruff on PATH is a different version (SW_ALLOW_TOOL_VERSION_DRIFT=1 for a
deliberate bump-and-reformat run). Bumping ruff now means editing _env.sh AND the
three workflows together, with the resulting fixes in the same commit.

Verified: run-lint.sh exit 0 ("ruff check ... All checks passed!") and
run-format.sh --check exit 0 ("8 files already formatted") under the pinned
0.15.21 — 0 new findings from the version change.
Negative control: forcing SW_RUFF_VERSION=9.9.9 makes run-lint.sh exit 1 on the
mismatch; the pinned value exits 0.

Not from this change: run-ci.sh reports CI FAIL (gates: SURFACE TEST) on this
branch. SURFACE is SEMVER-DIFF flagging 14 members removed since 3.0.0 by the
wave6 ctor-dunder-fold work, and TEST is the gradle build-cache FROM-CACHE
staleness check (`run-tests.sh --rerun-tasks` on this same tree is green:
executed=2331 failed=0). Neither can be caused by a workflow/_env.sh-only diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016i9TibArqEmDszG7VYEGYg
… reached through

PR #66 was `==> CI FAIL (gates: SURFACE )` on three rules — DRIFT, SURFACE-DIFF and
SURFACE-FRESH. Two root causes, both upstream of the port's hand-written code.

ENVELOPE NEVER EMITTED (DRIFT + SURFACE-DIFF). _build_swaig_actions read
`schemas["SwaigAction"]["properties"]` to lift the per-verb value objects
(ContextSwitchAction / HoldAction / PlaybackBgAction / TransferAction) and then
returned — so it reached THROUGH the envelope and never emitted `SwaigAction` or
`SwaigResponse` themselves, though swaig-response.yaml declares both in
components/schemas in their own right and the reference declares both as TypedDicts
(signalwire/core/swaig_actions_generated.py:48 and :83). The gates saw it from both
layers: DRIFT reported 5 missing-port members (SwaigAction.{context_switch,hold,
playback_bg,transfer} + SwaigResponse.action) and SURFACE-DIFF reported the 2 classes
missing outright.

This is one generator-side defect wearing per-port masks, not a java bug: go fixed
the identical reach-through in 41a012c and rust/php/ruby each root-caused to the same
place. The envelopes are also the cross-file $ref target post-prompt.yaml names for
post_response / delayed_post_response, which porting-sdk 4ddda70 taught the reference
generator to resolve.

Both envelopes are now emitted from the spec's OWN components/schemas, so their fields
are the schema's declared keys — SwaigAction carries all 27 action keys and
SwaigResponse carries {response, action, post_process}, matching the reference
TypedDicts key-for-key. A missing/propertyless envelope schema now raises SystemExit
rather than silently shrinking the emitted surface back to the pre-fix state.

STALE SURFACE ARTIFACT (SURFACE-FRESH). port_surface.json recorded
PostPromptSwaigLogEntry as carrying only `post_data`. The emitted java DTO already had
all three fields — post_data, post_response and delayed_post_response are present in
PostPromptSwaigLogEntry.java and match the reference (post_prompt_generated.py:217-219)
— so the CODE was right and the committed ARTIFACT was behind, another 4ddda70
fan-out. Regenerated signatures-then-surface in that order: enumerate_surface reads
port_signatures.json off disk, so the reverse order silently drops the new leaves while
both commands exit 0.

Nothing was omitted, allow-listed or excused; no floor was touched. Class count in the
generator docstring moved 4 -> 6 (total 20 -> 22) so it stays truthful.

ENUMERATOR VERIFIED COMPLETE, not merely present. SignatureDump's catch is already
narrowed to `Exception | LinkageError` (a bare Throwable would swallow OOM/StackOverflow
into a short-but-valid dump), and this run skipped ZERO classes — its stderr "skipping "
count is 0 across 143 modules / 7167 methods, and a second run reproduced the artifact
identically. The reds were real, not an enumerator that failed successfully.

Verification (porting-sdk pinned at wave6/ctor-dunder-fold @ 0322a5b, the
PORTING_SDK_REF the workflows check out):

  SURFACE suite, before:  [SURFACE] FAILED rules: DRIFT SURFACE-FRESH SURFACE-DIFF
  SURFACE suite, after:   all 9 rules PASS, exit 0
    [SURFACE:DRIFT] ... PASS        signatures match (1627 reference symbols,
                                    7190 port symbols)
    [SURFACE:SURFACE-DIFF] ... PASS port matches Python reference (2787 symbols)
    [SURFACE:SURFACE-FRESH] ... PASS
  GEN-FRESH (generate_swaig_payloads.py --check): exit 0, "generated SWAIG-payload
    files match porting-sdk/swaig-specs/*.yaml"
  run-format.sh: no-op on the generated output (8 files already formatted)

Negative control: reverting only the envelope emit block returns DRIFT to the same 5
missing-port findings and SURFACE-DIFF to the same 2 missing classes; reverting only
port_surface.json returns SURFACE-FRESH to the same 3 differing leaves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjEro9sSs5TLq66rTzSq6Z
Repoints java's RELAY-protocol generator off the legacy
`porting-sdk/relay-protocol/` directory onto the single document
`porting-sdk/combined-specs/relay.yaml`, following php (the R11 proof port).

build_outputs() loses its iterdir / `.params.json`-vs-`.result.json` suffix
split / `x-method`-with-filename-fallback / dedupe-by-filename block and
iterates the mapping the shared reader serves:

    RPS.shapes(psdk, phase) -> {method: schema_node}

`porting-sdk/scripts/relay_protocol_shapes.py`, loaded by file path exactly as
this script already loads generate_rest.py. The method name now comes from the
document's own key rather than an `x-method` field, and the phase from the block
the shape was carried in rather than a filename suffix.

Output is unchanged, per phase, at an exact bound:

  params  62 classes -> 62 (0)   318 properties -> 318 (0)
  result  61 classes -> 61 (0)   280 properties -> 280 (0)
  total  123 classes -> 123 (0)  598 properties -> 598 (0)

All 123 emitted files are byte-identical with NO provenance exception: java's
emitted header says "AUTO-GENERATED from porting-sdk/ (schemas)" and never named
the input directory. GEN-FRESH is green.

The docstring's "128 source files -> 126 params/result -> 126 - 3 = 123"
arithmetic was stale (there are 128 params/result shapes, not 126, and 5
property-less placeholders, not 3). Restated from the shapes themselves: 64
params less 2 property-less placeholders = 62, 64 result less 3 = 61, 62 + 61 =
123. The old text reached the right total by two compensating errors.

The combined document omits the `type: object` the per-file envelope declared;
`is_object_schema`'s `(type is None and properties)` branch covers it, so the
object-vs-alias verdict is unchanged.

`relay-protocol/` is not deleted; other generators still read it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjEro9sSs5TLq66rTzSq6Z
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant