From 2813ab3c4d0dad4416446277fe7c0435a550b345 Mon Sep 17 00:00:00 2001 From: Michael Jerris Date: Mon, 27 Jul 2026 01:28:06 -0400 Subject: [PATCH 01/74] wave6: retire dead ledger entries (logger idiom fold) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf --- PORT_SIGNATURE_OMISSIONS.md | 5 ----- port_signatures.json | 20 ++++++++++---------- port_surface_native.json | 5 ++++- scripts/enumerate_signatures.py | 23 +++++++++++++++++++++++ 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/PORT_SIGNATURE_OMISSIONS.md b/PORT_SIGNATURE_OMISSIONS.md index 9fff409..c92d6bd 100644 --- a/PORT_SIGNATURE_OMISSIONS.md +++ b/PORT_SIGNATURE_OMISSIONS.md @@ -68,7 +68,6 @@ Five-bucket classification (see PORTING_GUIDE.md): signalwire.agent_server.AgentServer.__init__: Java AgentServer collapses Python's host/port/log_level/event/context kwargs into typed runtime configuration (setters and gradle props); the same construction is reachable but takes a different shape signalwire.agent_server.AgentServer.app: Python AgentServer.app exposes the underlying ASGI application for embedding in test rigs; Java AgentServer wraps Javalin and does not expose the framework instance directly -signalwire.agent_server.AgentServer.logger: Python's reference exposes a logger attribute on the class for runtime introspection; Java threads logging through SLF4J without exposing a typed logger field signalwire.agent_server.AgentServer.register: Java AgentServer collapses Python's host/port/log_level/event/context kwargs into typed runtime configuration (setters and gradle props); the same construction is reachable but takes a different shape signalwire.agent_server.AgentServer.run: Java AgentServer collapses Python's host/port/log_level/event/context kwargs into typed runtime configuration (setters and gradle props); the same construction is reachable but takes a different shape signalwire.agent_server.AgentServer.unregister: Java AgentServer.unregister is fluent-self-returning for chaining; Python returns bool indicating whether the agent was registered @@ -118,10 +117,8 @@ signalwire.core.mixins.web_mixin.WebMixin.run: Java's WebMixin port collapses Py signalwire.core.mixins.web_mixin.WebMixin.serve: Java's WebMixin port collapses Python's host/port/ssl_*/event/context kwargs into typed runtime configuration (gradle/system-property driven); the same parity is reachable but not as method args signalwire.core.mixins.web_mixin.WebMixin.set_dynamic_config_callback: Java WebMixin.setDynamicConfigCallback takes the AgentBase reference directly so the lambda has typed access to the agent; Python takes a free-Callable that receives (data, headers, query, agent) as positional args signalwire.core.security.session_manager.SessionManager.__init__: Java SessionManager constructor is parameterless (token_expiry/secret are injected via setters or system properties); Python takes them as kwargs to __init__ -signalwire.core.skill_base.SkillBase.logger: Python's reference exposes a logger attribute on the class for runtime introspection; Java threads logging through SLF4J without exposing a typed logger field signalwire.core.skill_base.SkillBase.register_tools: Java SkillBase.registerTools returns a list of ToolDefinitions to register; Python returns void and the SkillBase implementation calls agent.define_tool() directly signalwire.core.skill_base.SkillBase.setup: Java SkillBase.setup takes a typed params map at attach time; Python's setup is parameterless (kwargs flow from the SkillManager.add_skill call) -signalwire.core.skill_manager.SkillManager.logger: Python's reference exposes a logger attribute on the class for runtime introspection; Java threads logging through SLF4J without exposing a typed logger field signalwire.core.swml_service.SWMLService.__init__: Java SWMLService takes only (name, route) on construction and serves with no args; the host/port/basic_auth/schema_path/config_file/schema_validation/ssl options arrive via setters or runtime config signalwire.core.swml_service.SWMLService.get_basic_auth_credentials: Java exposes a separate getBasicAuthCredentialsWithSource() overload for the include_source case and returns String[] (a list); Python takes include_source as a kwarg and returns a tuple/3-tuple signalwire.core.swml_service.SWMLService.serve: Java SWMLService takes only (name, route) on construction and serves with no args; the host/port/basic_auth/schema_path/config_file/schema_validation/ssl options arrive via setters or runtime config @@ -241,7 +238,6 @@ signalwire.rest._base.SignalWireRestError.__init__: Java's error constructor car signalwire.rest._base.SignalWireRestTransportError.__init__: Java's SignalWireRestTransportError(method, path, url, cause) takes the same method/path/url envelope as RestError plus an explicit Throwable cause (the java-idiom equivalent of Python's `raise ... from exc`), instead of Python's positional (body, url, method) with no separate exception param — the body message is derived from cause.getMessage() rather than passed in directly, and statusCode is fixed at the family's NO_STATUS (0) sentinel rather than omitted (plan 1.3b) signalwire.skills.mcp_gateway.skill.MCPGatewaySkill.register_tools: Java built-in skill idiom — registerTools() RETURNS a List for the SkillManager to register with the agent, whereas Python's register_tools returns void and calls self.define_tool() directly (same divergence recorded at signalwire.core.skill_base.SkillBase.register_tools). Wire/surface identical; the SURFACE gate matches. signalwire.skills.mcp_gateway.skill.MCPGatewaySkill.setup: Java built-in skill idiom — setup(Map params) takes the typed params map at attach time, whereas Python's setup(self) is parameterless and reads self.params (kwargs flow from SkillManager.add_skill). Same divergence recorded at signalwire.core.skill_base.SkillBase.setup; behavior-identical. -signalwire.skills.registry.SkillRegistry.logger: Python's reference exposes a logger attribute on the class for runtime introspection; Java threads logging through SLF4J without exposing a typed logger field signalwire.skills.spider.skill.SpiderSkill.__init__: Java built-in skill constructor is parameterless (params arrive at setup() time per Java's skill lifecycle); Python takes (agent, params) on construction ## POM (signalwire.pom.pom) — Java idiom @@ -269,7 +265,6 @@ signalwire.core.config_loader.ConfigLoader.find_config_file: Java-idiom signatur signalwire.core.config_loader.ConfigLoader.merge_with_env: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.core.config_loader.ConfigLoader.substitute_vars: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.core.function_result.FunctionResult.join_conference: Java-idiom signature divergence: a typed functional-interface / boxed scalar / concrete type stands in for Python's generic Callable / union / dynamic type; wire behavior identical -signalwire.core.logging_config.get_logger: Java-idiom fold: the port exposes this reference member through a differently-named accessor / folds it onto a related class; covered at the surface layer (rename/projection), so the raw signature name is absent here signalwire.core.logging_config.strip_control_chars: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.core.mixins.ai_config_mixin.AIConfigMixin.add_mcp_server: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.core.mixins.tool_mixin.ToolMixin.define_tools: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical diff --git a/port_signatures.json b/port_signatures.json index 9f4b8e3..4d260de 100644 --- a/port_signatures.json +++ b/port_signatures.json @@ -4881,16 +4881,6 @@ "params": [], "returns": "class:signalwire.logging.Logger" }, - "get_logger": { - "params": [ - { - "name": "clazz", - "type": "any", - "required": true - } - ], - "returns": "class:signalwire.logging.Logger" - }, "info": { "params": [ { @@ -4955,6 +4945,16 @@ "params": [], "returns": "string" }, + "get_logger": { + "params": [ + { + "name": "name", + "type": "string", + "required": true + } + ], + "returns": "any" + }, "reset_logging_configuration": { "params": [], "returns": "void" diff --git a/port_surface_native.json b/port_surface_native.json index 3809384..3cb76c2 100644 --- a/port_surface_native.json +++ b/port_surface_native.json @@ -1,5 +1,5 @@ { - "generated_from": "signalwire-java @ 2b848d283a2d33e7b177be4e430b6af8c9c87a78", + "generated_from": "signalwire-java @ 4dd76a798edaad244264869da21026ab58ae680d", "language": "java", "modules": { "signalwire.agent.agent_base_builder": { @@ -1282,6 +1282,8 @@ "AIPostPromptText": [], "AIPromptPom": [], "AIPromptText": [], + "AiSidecar": [], + "AiSidecarConfig": [], "AllOfProperty": [], "AmazonBedrock": [], "AmazonBedrockObject": [], @@ -1381,6 +1383,7 @@ "Request": [], "RequestConfig": [], "Return": [], + "RingbackConfig": [], "SIPRefer": [], "SMSWithBody": [], "SMSWithMedia": [], diff --git a/scripts/enumerate_signatures.py b/scripts/enumerate_signatures.py index c0330c1..9f51513 100644 --- a/scripts/enumerate_signatures.py +++ b/scripts/enumerate_signatures.py @@ -634,6 +634,15 @@ def _pkg_to_module_with_class(pkg: str, class_name: str) -> str: ("signalwire.core.logging_config", "get_execution_mode"), # logging_config module-level free functions grouped on Logger's static # helpers (mirrors _FREE_FUNCTION_SURFACE_PROJECTIONS in enumerate_surface.py). + # ``getLogger`` is overloaded (String name / Class clazz); the generic + # overload-collapse tie-break (equal arity) would pick the Class one and + # emit ``get_logger(clazz: any)``, so its canonical shape is pinned via + # FREE_FUNCTION_SIGNATURE_OVERRIDES below to the reference's + # ``get_logger(name: string) -> any`` (the String overload). Keeping this + # line here restores lockstep with enumerate_surface.py, which has always + # projected ("Logger", "getLogger"). + ("com.signalwire.sdk.logging.Logger", "getLogger"): + ("signalwire.core.logging_config", "get_logger"), ("com.signalwire.sdk.logging.Logger", "configureLogging"): ("signalwire.core.logging_config", "configure_logging"), ("com.signalwire.sdk.logging.Logger", "resetLoggingConfiguration"): @@ -727,6 +736,20 @@ def _pkg_to_module_with_class(pkg: str, class_name: str) -> str: # param is keyword-only in the Python reference (``*, signing_key``); recording # it as ``kind: keyword`` keeps the drift compare exact. FREE_FUNCTION_SIGNATURE_OVERRIDES: dict[tuple[str, str], dict] = { + # get_logger(name) -> logger. Java's Logger.getLogger is overloaded + # (String name / Class clazz) and both overloads have arity 1, so the + # generic fewer-param collapse cannot separate them and the Class one + # wins the tie — emitting ``get_logger(clazz: any)``. Pin the String + # overload, which is the reference's shape. The Class overload stays a + # Java-idiom convenience the overload-collapse drops (same model as every + # other Java overload); the returned Logger is the port's logger handle, + # recorded as the oracle's ``any``. + ("com.signalwire.sdk.logging.Logger", "getLogger"): { + "params": [ + {"name": "name", "type": "string", "required": True}, + ], + "returns": "any", + }, ("com.signalwire.sdk.security.WebhookValidator", "validate"): { "params": [ {"name": "method", "type": "string", "required": True}, From 520af80d1e3c5468801150435ae1af1c8978ef5e Mon Sep 17 00:00:00 2001 From: Michael Jerris Date: Mon, 27 Jul 2026 06:43:50 -0400 Subject: [PATCH 02/74] fix(enumerate_surface): fail loud instead of silently writing nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--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 ` line to stderr on success, matching php/rust/cpp/dotnet/ perl/ts. Coordinated-With: signalwire/porting-sdk# 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) Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf --- scripts/enumerate_surface.py | 43 +++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/scripts/enumerate_surface.py b/scripts/enumerate_surface.py index 067866e..e207fa3 100755 --- a/scripts/enumerate_surface.py +++ b/scripts/enumerate_surface.py @@ -28,9 +28,19 @@ ``audit_docs.py``, which extracts method-call patterns from ``docs/`` and ``examples/*.java`` in their natural Java form. +An output destination is MANDATORY — ``--output PATH`` (write the file) or +``--stdout`` (pipe it). A bare run FAILS LOUD with a usage error. It used to +default to stdout, and that default was a silent wrong-green: a bare run 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" (it cost a 7-port surface-audit red). php / rust / +cpp / dotnet / perl / ts / go all write the file rather than defaulting to a pipe; +this makes Java's enumerator stop being the fleet's one exception. + Usage:: - python3 scripts/enumerate_surface.py # stdout + python3 scripts/enumerate_surface.py # ERROR: names no destination + python3 scripts/enumerate_surface.py --stdout # explicit pipe python3 scripts/enumerate_surface.py --output port_surface.json python3 scripts/enumerate_surface.py --check --output port_surface.json python3 scripts/enumerate_surface.py --native --output port_surface_native.json @@ -2021,7 +2031,12 @@ def main(argv: list[str]) -> int: ) parser.add_argument( "--output", type=Path, default=None, - help="Write JSON to this path (default: stdout)", + help="Write JSON to this path. Required unless --stdout is given.", + ) + parser.add_argument( + "--stdout", action="store_true", + help="Print JSON to stdout instead of writing --output. Explicit opt-in: " + "there is no stdout DEFAULT (see the fail-loud check below).", ) parser.add_argument( "--check", action="store_true", @@ -2037,6 +2052,23 @@ def main(argv: list[str]) -> int: if args.check and not args.output: parser.error("--check requires --output") + if args.stdout and args.output: + parser.error("--stdout and --output are mutually exclusive") + if args.check and args.stdout: + parser.error("--check compares against --output; it cannot be used with --stdout") + # FAIL LOUD on a bare run. This used to default to stdout, which made a bare + # invocation a silent WRONG-GREEN: it 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". That cost + # a 7-port surface-audit red. Writing the file is now never implicit and neither is + # piping: state one. + if not args.output and not args.stdout: + parser.error( + "no output destination: pass --output PATH to write the surface JSON, or " + "--stdout to pipe it. There is deliberately no default — a bare run used " + "to print to stdout and write nothing while exiting 0, which reads as " + "'no change was needed' when it means 'nothing was written'." + ) if not args.reference.is_file(): print(f"error: reference {args.reference} not found", file=sys.stderr) return 1 @@ -2066,10 +2098,11 @@ def strip_meta(s: str) -> str: return 1 return 0 - if args.output: - args.output.write_text(rendered, encoding="utf-8") - else: + if args.stdout: sys.stdout.write(rendered) + else: + args.output.write_text(rendered, encoding="utf-8") + print(f"wrote {args.output}", file=sys.stderr) return 0 From 97f14d60a891a090f0a3212382ad3ab3376aa719 Mon Sep 17 00:00:00 2001 From: Michael Jerris Date: Mon, 27 Jul 2026 09:49:10 -0400 Subject: [PATCH 03/74] =?UTF-8?q?wave6:=20retire=20dead=20ctor=20entries?= =?UTF-8?q?=20(ALLOWLIST=5FDISCIPLINE=20=C2=A7495,=20shared-diff=20fold)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01GFKJhLvfV8yGrASwqxdgaf --- PORT_SIGNATURE_OMISSIONS.md | 62 ------------------------------------- 1 file changed, 62 deletions(-) diff --git a/PORT_SIGNATURE_OMISSIONS.md b/PORT_SIGNATURE_OMISSIONS.md index 9fff409..e1b6210 100644 --- a/PORT_SIGNATURE_OMISSIONS.md +++ b/PORT_SIGNATURE_OMISSIONS.md @@ -66,7 +66,6 @@ Five-bucket classification (see PORTING_GUIDE.md): # Format: `: ` -signalwire.agent_server.AgentServer.__init__: Java AgentServer collapses Python's host/port/log_level/event/context kwargs into typed runtime configuration (setters and gradle props); the same construction is reachable but takes a different shape signalwire.agent_server.AgentServer.app: Python AgentServer.app exposes the underlying ASGI application for embedding in test rigs; Java AgentServer wraps Javalin and does not expose the framework instance directly signalwire.agent_server.AgentServer.logger: Python's reference exposes a logger attribute on the class for runtime introspection; Java threads logging through SLF4J without exposing a typed logger field signalwire.agent_server.AgentServer.register: Java AgentServer collapses Python's host/port/log_level/event/context kwargs into typed runtime configuration (setters and gradle props); the same construction is reachable but takes a different shape @@ -85,14 +84,10 @@ signalwire.core.agent_base.AgentBase.on_debug_event: Java AgentBase.onDebugEvent signalwire.core.agent_base.AgentBase.on_summary: Java AgentBase.onSummary registers a typed callback returning AgentBase (this) for fluent chaining; Python's on_summary takes summary and raw_data positionally and is a separate callback-shape signalwire.core.agent_base.AgentBase.skill_manager: Python AgentBase.skill_manager exposes the per-agent skill manager as a public attribute; Java AgentBase routes the same surface through getSkillManager() signalwire.core.contexts.Context.add_step: Java Context.addStep takes only the step name; the step's task/bullets/criteria/functions/valid_steps are configured via fluent methods on the returned Step. Python takes them all as kwargs to add_step -signalwire.core.contexts.ContextBuilder.__init__: Java ContextBuilder is a static-factory class with a no-arg constructor; Python's ContextBuilder takes the owning agent as a positional arg -signalwire.core.contexts.GatherInfo.__init__: Java GatherInfo uses a Builder for output_key/completion_action/prompt; Python takes them as kwargs to __init__ -signalwire.core.contexts.GatherQuestion.__init__: Java GatherQuestion takes only key+question; type/confirm/prompt/functions are configured via fluent methods. Python takes them as kwargs signalwire.core.contexts.Step.add_gather_question: Java Step.addGatherQuestion takes only key+question; the rest are configured fluently on the returned GatherQuestion. Python takes them as kwargs signalwire.core.data_map.DataMap.expression: Java DataMap collapses Python kwargs into typed-builder helpers (expression/parameter/webhook overload signatures with fewer named args); the optional-arg surface is reachable via additional fluent setters signalwire.core.data_map.DataMap.parameter: Java DataMap collapses Python kwargs into typed-builder helpers (expression/parameter/webhook overload signatures with fewer named args); the optional-arg surface is reachable via additional fluent setters signalwire.core.data_map.DataMap.webhook: Java DataMap collapses Python kwargs into typed-builder helpers (expression/parameter/webhook overload signatures with fewer named args); the optional-arg surface is reachable via additional fluent setters -signalwire.core.function_result.FunctionResult.__init__: Java FunctionResult action methods collapse the Python kwarg-rich signature into a typed Builder + method-on-builder shape; the optional args (timeout/post_process/etc.) move to the builder signalwire.core.function_result.FunctionResult.connect: Java FunctionResult action methods collapse the Python kwarg-rich signature into a typed Builder + method-on-builder shape; the optional args (timeout/post_process/etc.) move to the builder signalwire.core.function_result.FunctionResult.execute_rpc: Java FunctionResult action methods collapse the Python kwarg-rich signature into a typed Builder + method-on-builder shape; the optional args (timeout/post_process/etc.) move to the builder signalwire.core.function_result.FunctionResult.execute_swml: Java FunctionResult action methods collapse the Python kwarg-rich signature into a typed Builder + method-on-builder shape; the optional args (timeout/post_process/etc.) move to the builder @@ -117,24 +112,14 @@ signalwire.core.mixins.web_mixin.WebMixin.on_swml_request: Java's WebMixin port signalwire.core.mixins.web_mixin.WebMixin.run: Java's WebMixin port collapses Python's host/port/ssl_*/event/context kwargs into typed runtime configuration (gradle/system-property driven); the same parity is reachable but not as method args signalwire.core.mixins.web_mixin.WebMixin.serve: Java's WebMixin port collapses Python's host/port/ssl_*/event/context kwargs into typed runtime configuration (gradle/system-property driven); the same parity is reachable but not as method args signalwire.core.mixins.web_mixin.WebMixin.set_dynamic_config_callback: Java WebMixin.setDynamicConfigCallback takes the AgentBase reference directly so the lambda has typed access to the agent; Python takes a free-Callable that receives (data, headers, query, agent) as positional args -signalwire.core.security.session_manager.SessionManager.__init__: Java SessionManager constructor is parameterless (token_expiry/secret are injected via setters or system properties); Python takes them as kwargs to __init__ signalwire.core.skill_base.SkillBase.logger: Python's reference exposes a logger attribute on the class for runtime introspection; Java threads logging through SLF4J without exposing a typed logger field signalwire.core.skill_base.SkillBase.register_tools: Java SkillBase.registerTools returns a list of ToolDefinitions to register; Python returns void and the SkillBase implementation calls agent.define_tool() directly signalwire.core.skill_base.SkillBase.setup: Java SkillBase.setup takes a typed params map at attach time; Python's setup is parameterless (kwargs flow from the SkillManager.add_skill call) signalwire.core.skill_manager.SkillManager.logger: Python's reference exposes a logger attribute on the class for runtime introspection; Java threads logging through SLF4J without exposing a typed logger field -signalwire.core.swml_service.SWMLService.__init__: Java SWMLService takes only (name, route) on construction and serves with no args; the host/port/basic_auth/schema_path/config_file/schema_validation/ssl options arrive via setters or runtime config signalwire.core.swml_service.SWMLService.get_basic_auth_credentials: Java exposes a separate getBasicAuthCredentialsWithSource() overload for the include_source case and returns String[] (a list); Python takes include_source as a kwarg and returns a tuple/3-tuple signalwire.core.swml_service.SWMLService.serve: Java SWMLService takes only (name, route) on construction and serves with no args; the host/port/basic_auth/schema_path/config_file/schema_validation/ssl options arrive via setters or runtime config -signalwire.prefabs.concierge.ConciergeAgent.__init__: Java prefab constructor takes a smaller set of typed positional args; the rest of the Python kwargs (services/amenities/hours/etc.) are configured via fluent setters on the returned agent -signalwire.prefabs.faq_bot.FAQBotAgent.__init__: Java prefab constructor takes a smaller set of typed positional args; the rest of the Python kwargs (services/amenities/hours/etc.) are configured via fluent setters on the returned agent -signalwire.prefabs.info_gatherer.InfoGathererAgent.__init__: Java prefab constructor takes a smaller set of typed positional args; the rest of the Python kwargs (services/amenities/hours/etc.) are configured via fluent setters on the returned agent -signalwire.prefabs.receptionist.ReceptionistAgent.__init__: Java prefab constructor takes a smaller set of typed positional args; the rest of the Python kwargs (services/amenities/hours/etc.) are configured via fluent setters on the returned agent -signalwire.prefabs.survey.SurveyAgent.__init__: Java prefab constructor takes a smaller set of typed positional args; the rest of the Python kwargs (services/amenities/hours/etc.) are configured via fluent setters on the returned agent -signalwire.relay.call.AIAction.__init__: Java relay Action subclass constructor takes (controlId, call) — id-first identifier, owner second; Python takes (call, control_id) — the order swap is the canonical Java convention signalwire.relay.call.AIAction.stop: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally -signalwire.relay.call.Action.__init__: Java relay Action constructor takes (controlId, call) — id-first identifier, owner second; Python takes (call, control_id, terminal_event, terminal_states). The terminal-event metadata is registered separately in Java signalwire.relay.call.Action.result: Python relay Action.result is a future-result attribute populated by the eventing thread; Java models the same async-completion pattern via CompletableFuture on each method without an attribute hand-off -signalwire.relay.call.Call.__init__: Java Call constructor is internal (created by RelayClient on event dispatch); the (client, call_id, node_id, etc.) Python construction args are passed through Java's relay-event factory instead signalwire.relay.call.Call.ai: Java Call action method takes a typed Config object that bundles Python's kwargs (volume/loop/control_id/on_completed/etc.) AND returns a typed Action subclass (PlayAction/CollectAction/etc.) for typed-fluent control; Python takes positional kwargs and returns the generic Action base signalwire.relay.call.Call.ai_hold: Java Call action method takes a typed Config/Options object that bundles Python's kwargs (volume/loop/control_id/on_completed/etc.); the same surface is reachable through the Config builder signalwire.relay.call.Call.ai_message: Java Call action method takes a typed Config/Options object that bundles Python's kwargs (volume/loop/control_id/on_completed/etc.); the same surface is reachable through the Config builder @@ -178,34 +163,25 @@ signalwire.relay.call.Call.user_event: Java Call action method takes a typed Con signalwire.relay.call.Call.wait_for_answered: omitted — no relay Call state-wait primitive in the Java port (the generic wait_for / wait_for_ended are themselves omitted in PORT_OMISSIONS.md as having no direct Java analog), so there is nothing to build the typed state-wait on signalwire.relay.call.Call.wait_for_ending: omitted — no relay Call state-wait primitive in the Java port (the generic wait_for / wait_for_ended are themselves omitted in PORT_OMISSIONS.md as having no direct Java analog), so there is nothing to build the typed state-wait on signalwire.relay.call.Call.wait_for_ringing: omitted — no relay Call state-wait primitive in the Java port (the generic wait_for / wait_for_ended are themselves omitted in PORT_OMISSIONS.md as having no direct Java analog), so there is nothing to build the typed state-wait on -signalwire.relay.call.CollectAction.__init__: Java relay Action subclass constructor takes (controlId, call) — id-first identifier, owner second; Python takes (call, control_id) — the order swap is the canonical Java convention signalwire.relay.call.CollectAction.pause: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally signalwire.relay.call.CollectAction.resume: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally signalwire.relay.call.CollectAction.start_input_timers: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally signalwire.relay.call.CollectAction.stop: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally signalwire.relay.call.CollectAction.volume: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally -signalwire.relay.call.DetectAction.__init__: Java relay Action subclass constructor takes (controlId, call) — id-first identifier, owner second; Python takes (call, control_id) — the order swap is the canonical Java convention signalwire.relay.call.DetectAction.stop: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally signalwire.relay.call.FaxAction.stop: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally -signalwire.relay.call.PayAction.__init__: Java relay Action subclass constructor takes (controlId, call) — id-first identifier, owner second; Python takes (call, control_id) — the order swap is the canonical Java convention signalwire.relay.call.PayAction.stop: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally -signalwire.relay.call.PlayAction.__init__: Java relay Action subclass constructor takes (controlId, call) — id-first identifier, owner second; Python takes (call, control_id) — the order swap is the canonical Java convention signalwire.relay.call.PlayAction.pause: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally signalwire.relay.call.PlayAction.resume: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally signalwire.relay.call.PlayAction.stop: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally signalwire.relay.call.PlayAction.volume: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally -signalwire.relay.call.RecordAction.__init__: Java relay Action subclass constructor takes (controlId, call) — id-first identifier, owner second; Python takes (call, control_id) — the order swap is the canonical Java convention signalwire.relay.call.RecordAction.pause: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally signalwire.relay.call.RecordAction.resume: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally signalwire.relay.call.RecordAction.stop: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally -signalwire.relay.call.StandaloneCollectAction.__init__: Java relay Action subclass constructor takes (controlId, call) — id-first identifier, owner second; Python takes (call, control_id) — the order swap is the canonical Java convention signalwire.relay.call.StandaloneCollectAction.start_input_timers: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally signalwire.relay.call.StandaloneCollectAction.stop: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally -signalwire.relay.call.StreamAction.__init__: Java relay Action subclass constructor takes (controlId, call) — id-first identifier, owner second; Python takes (call, control_id) — the order swap is the canonical Java convention signalwire.relay.call.StreamAction.stop: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally -signalwire.relay.call.TapAction.__init__: Java relay Action subclass constructor takes (controlId, call) — id-first identifier, owner second; Python takes (call, control_id) — the order swap is the canonical Java convention signalwire.relay.call.TapAction.stop: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally -signalwire.relay.call.TranscribeAction.__init__: Java relay Action subclass constructor takes (controlId, call) — id-first identifier, owner second; Python takes (call, control_id) — the order swap is the canonical Java convention signalwire.relay.call.TranscribeAction.stop: Java relay Action sub-action method returns the request payload as Map for caller inspection; Python returns void as the same payload is materialised internally signalwire.relay.client.RelayClient.dial: Java RelayClient.dial takes only the device list; tag/max_duration/dial_timeout move to a typed Options or setter pair in Java signalwire.relay.client.RelayClient.on_call: Java RelayClient.onCall/onMessage takes a typed CallHandler/MessageHandler functional interface and returns it (caller can detach via off()); Python takes a generic Callable[[Call|Message], None] and returns void @@ -213,44 +189,16 @@ signalwire.relay.client.RelayClient.on_message: Java RelayClient.onCall/onMessag signalwire.relay.client.RelayClient.receive: Java RelayClient.receive/unreceive returns the relay-server response payload as Map for caller inspection; Python returns void and surfaces errors via exceptions signalwire.relay.client.RelayClient.send_message: Java RelayClient.sendMessage takes (context, from_number, to_number, body, media, tags); Python adds region and on_completed kwargs that move to a typed Options/Listener in Java signalwire.relay.client.RelayClient.unreceive: Java RelayClient.receive/unreceive returns the relay-server response payload as Map for caller inspection; Python returns void and surfaces errors via exceptions -signalwire.relay.client.RelayError.__init__: Java RelayError takes only the message; the optional code arg is set via setError() or routed through the typed RelayException hierarchy in Java -signalwire.relay.event.CallReceiveEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.CallStateEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.CollectEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.ConferenceEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.ConnectEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.DetectEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.DialEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.FaxEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.MessageReceiveEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.MessageStateEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.PayEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.PlayEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.QueueEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.RecordEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.ReferEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.RelayEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.SendDigitsEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.StreamEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.TapEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.event.TranscribeEvent.__init__: Java relay event constructors take only the four header fields (event_type, timestamp, params, plus call_id where present); Python flattens the typed event payload (call_state/direction/etc.) into __init__ params, while Java keeps it accessible via getters on the typed event subclass -signalwire.relay.message.Message.__init__: Java Message constructor is internal (created by RelayClient on event dispatch); the typed message_id/context/direction/etc. arrive via setters or Builder pre-emit signalwire.rest._base.HttpClient.post: Java's typed CRUD/HTTP base classes accept the optional params kwarg via a typed Map; the additional fluent overloads cover the no-params case. Python takes params as a kwarg on the single method -signalwire.rest._pagination.PaginatedIterator.__init__: Java's typed CRUD/HTTP base classes accept the optional params kwarg via a typed Map; the additional fluent overloads cover the no-params case. Python takes params as a kwarg on the single method -signalwire.rest._base.SignalWireRestError.__init__: Java's error constructor carries the full failure envelope (statusCode, method, path, url, responseBody) — a superset of Python's (status_code, body, url, method). It keeps the java-idiom getPath() backing field (a documented accessor, see PORT_ADDITIONS) ALONGSIDE the reference's url (item 1.3a), so the constructor param list is one wider than Python's -signalwire.rest._base.SignalWireRestTransportError.__init__: Java's SignalWireRestTransportError(method, path, url, cause) takes the same method/path/url envelope as RestError plus an explicit Throwable cause (the java-idiom equivalent of Python's `raise ... from exc`), instead of Python's positional (body, url, method) with no separate exception param — the body message is derived from cause.getMessage() rather than passed in directly, and statusCode is fixed at the family's NO_STATUS (0) sentinel rather than omitted (plan 1.3b) signalwire.skills.mcp_gateway.skill.MCPGatewaySkill.register_tools: Java built-in skill idiom — registerTools() RETURNS a List for the SkillManager to register with the agent, whereas Python's register_tools returns void and calls self.define_tool() directly (same divergence recorded at signalwire.core.skill_base.SkillBase.register_tools). Wire/surface identical; the SURFACE gate matches. signalwire.skills.mcp_gateway.skill.MCPGatewaySkill.setup: Java built-in skill idiom — setup(Map params) takes the typed params map at attach time, whereas Python's setup(self) is parameterless and reads self.params (kwargs flow from SkillManager.add_skill). Same divergence recorded at signalwire.core.skill_base.SkillBase.setup; behavior-identical. signalwire.skills.registry.SkillRegistry.logger: Python's reference exposes a logger attribute on the class for runtime introspection; Java threads logging through SLF4J without exposing a typed logger field -signalwire.skills.spider.skill.SpiderSkill.__init__: Java built-in skill constructor is parameterless (params arrive at setup() time per Java's skill lifecycle); Python takes (agent, params) on construction ## POM (signalwire.pom.pom) — Java idiom -signalwire.pom.pom.PromptObjectModel.__init__: java-builder-ctors — Java exposes overloaded ctors (default, copy-from-list, copy-from-PromptObjectModel) where Python has a single __init__ with default arg signalwire.pom.pom.PromptObjectModel.add_section: java-builder-overload — Java exposes 3 overloads (title-only, title+kwargs, full builder pattern) where Python has a single positional+kwargs signature signalwire.pom.pom.PromptObjectModel.from_json: java-overload-pair — Java pair of static factories (from_json(String), from_json_map(List)) where Python's from_json takes Union[str, dict] at one signature signalwire.pom.pom.PromptObjectModel.from_yaml: java-overload-pair — Java pair of static factories (from_yaml(String), from_yaml_map(List)) where Python's from_yaml takes Union[str, dict] at one signature -signalwire.pom.pom.Section.__init__: java-builder-ctors — Java exposes overloaded ctors (default, builder, copy) where Python has a single __init__ with positional+kwargs signalwire.pom.pom.Section.add_subsection: java-builder-overload — Java exposes 2 overloads (title-only, title+kwargs) where Python has single positional+kwargs signalwire.pom.pom.Section.render_markdown: java-overload — Java has overloads with/without sectionNumber list parameter; Python uses optional default signalwire.pom.pom.Section.render_xml: java-overload — Java has overloads with/without indent + sectionNumber parameters; Python uses optional defaults @@ -258,13 +206,10 @@ signalwire.pom.pom.Section.render_xml: java-overload — Java has overloads with # --- item H/I subsystem signature divergences (appended) --- signalwire.agent_server.AgentServer.setup_sip_routing: Java-idiom: setupSipRouting()/setupSipRouting(route,autoMap) overloads; the no-arg form enumerates first. Python's single (route, auto_map) signature is covered by the 2-arg overload — param-count differs by overload -signalwire.core.agent.prompt.manager.PromptManager.__init__: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical -signalwire.core.agent.tools.registry.ToolRegistry.__init__: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.core.auth_handler.AuthHandler.flask_decorator: Java-idiom signature divergence: a typed functional-interface / boxed scalar / concrete type stands in for Python's generic Callable / union / dynamic type; wire behavior identical signalwire.core.auth_handler.AuthHandler.get_fastapi_dependency: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.core.auth_handler.AuthHandler.verify_basic_auth: Java-idiom signature divergence: a typed functional-interface / boxed scalar / concrete type stands in for Python's generic Callable / union / dynamic type; wire behavior identical signalwire.core.auth_handler.AuthHandler.verify_bearer_token: Java-idiom signature divergence: a typed functional-interface / boxed scalar / concrete type stands in for Python's generic Callable / union / dynamic type; wire behavior identical -signalwire.core.config_loader.ConfigLoader.__init__: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.core.config_loader.ConfigLoader.find_config_file: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.core.config_loader.ConfigLoader.merge_with_env: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.core.config_loader.ConfigLoader.substitute_vars: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical @@ -278,7 +223,6 @@ signalwire.core.pom_builder.PomBuilder.add_section: Java-idiom signature diverge signalwire.core.pom_builder.PomBuilder.add_subsection: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.core.security.security_utils.filter_sensitive_headers: Java-idiom signature divergence: a typed functional-interface / boxed scalar / concrete type stands in for Python's generic Callable / union / dynamic type; wire behavior identical signalwire.core.security.session_manager.SessionManager.create_session: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical -signalwire.core.security_config.SecurityConfig.__init__: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.core.security_config.SecurityConfig.get_basic_auth: Java-idiom signature divergence: fluent self-return (or a concrete typed return / small record) where Python returns void / a tuple / a dynamic value; wire behavior identical signalwire.core.security_config.SecurityConfig.get_security_headers: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.core.security_config.SecurityConfig.validate_ssl_config: Java-idiom signature divergence: fluent self-return (or a concrete typed return / small record) where Python returns void / a tuple / a dynamic value; wire behavior identical @@ -317,7 +261,6 @@ signalwire.register_skill: Java-idiom signature divergence: a typed functional-i signalwire.relay.call.Action.wait: Java-idiom fold: the port exposes this reference member through a differently-named accessor / folds it onto a related class; covered at the surface layer (rename/projection), so the raw signature name is absent here signalwire.relay.call.Call.wait_for: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.relay.call.Call.wait_for_ended: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical -signalwire.relay.call.FaxAction.__init__: Java relay Action subclass constructor takes (controlId, call) — id-first identifier, owner second; Python takes (call, control_id, method_prefix). The send/receive method prefix is fixed per Java subclass (SendFaxAction/ReceiveFaxAction) instead of a constructor arg signalwire.relay.event.CallReceiveEvent.from_payload: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.relay.event.CallStateEvent.from_payload: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.relay.event.CallingErrorEvent.from_payload: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical @@ -377,12 +320,7 @@ signalwire.skills.wikipedia_search.skill.WikipediaSearchSkill.register_tools: si signalwire.skills.wikipedia_search.skill.WikipediaSearchSkill.setup: sig-oracle blind spot (L12): the signature oracle records no method here (it did not capture the subclass override / property), while the SURFACE oracle does and the surface gate matches it — the method is real, present, and behavior-correct; this is a port-only signature only against the narrower signature oracle signalwire.utils.schema_utils.SchemaUtils.generate_method_body: sig-oracle blind spot (L12): the signature oracle records no method here (it did not capture the subclass override / property), while the SURFACE oracle does and the surface gate matches it — the method is real, present, and behavior-correct; this is a port-only signature only against the narrower signature oracle signalwire.utils.schema_utils.SchemaUtils.generate_method_signature: sig-oracle blind spot (L12): the signature oracle records no method here (it did not capture the subclass override / property), while the SURFACE oracle does and the surface gate matches it — the method is real, present, and behavior-correct; this is a port-only signature only against the narrower signature oracle -signalwire.web.web_service.WebService.__init__: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical signalwire.web.web_service.WebService.start: Java-idiom signature divergence: the port collapses Python's optional kwargs into a builder / explicit-overload / no-arg+setter shape (or takes no back-reference `agent` param — the manager is standalone), so the param count differs; wire behavior is identical -signalwire.relay.event.CallingErrorEvent.__init__: Java-idiom: the event constructor takes (eventType, timestamp, params) and reads code/message/call_id out of the params map; Python spreads them as explicit __init__ args. Param count differs; wire payload identical. -signalwire.relay.event.DenoiseEvent.__init__: Java-idiom: the event constructor takes (eventType, timestamp, params) and reads denoised/call_id out of the params map; Python spreads them as explicit __init__ args. Param count differs; wire payload identical. -signalwire.relay.event.EchoEvent.__init__: Java-idiom: the event constructor takes (eventType, timestamp, params) and reads state/call_id out of the params map; Python spreads them as explicit __init__ args. Param count differs; wire payload identical. -signalwire.relay.event.HoldEvent.__init__: Java-idiom: the event constructor takes (eventType, timestamp, params) and reads state/call_id out of the params map; Python spreads them as explicit __init__ args. Param count differs; wire payload identical. # ── SIGNATURE ADDITIONS surviving the accessor-fold / dunder-exclude / DTO-emit # passes: genuine port-only signatures with NO same-class reference twin (typed From 6e6f78afab2b18d563568605c769fae9958ad669 Mon Sep 17 00:00:00 2001 From: Michael Jerris Date: Mon, 27 Jul 2026 12:49:42 -0400 Subject: [PATCH 04/74] wave6: expose the 7 derived caller-observable attrs (java) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- port_signatures.json | 90 +++++++++++++++---- port_surface.json | 10 ++- port_surface_native.json | 16 +++- .../java/com/signalwire/sdk/relay/Action.java | 13 +++ .../sdk/skills/builtin/SpiderSkill.java | 74 +++++++++++++-- .../java/com/signalwire/sdk/swml/Service.java | 49 ++++++++++ 6 files changed, 224 insertions(+), 28 deletions(-) diff --git a/port_signatures.json b/port_signatures.json index 4d260de..68dde95 100644 --- a/port_signatures.json +++ b/port_signatures.json @@ -10587,6 +10587,15 @@ ], "returns": "class:signalwire.core.security_config.SecurityConfig" }, + "domain": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, "host": { "params": [ { @@ -10622,6 +10631,33 @@ } ], "returns": "string" + }, + "ssl_cert_path": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "ssl_key_path": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" + }, + "ssl_enabled": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" } } } @@ -19632,6 +19668,15 @@ ], "returns": "class:signalwire.relay.call.Call" }, + "completed": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "bool" + }, "control_id": { "params": [ { @@ -30601,15 +30646,6 @@ ], "returns": "string" }, - "get_request_id": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "optional" - }, "get_response_body": { "params": [ { @@ -30682,6 +30718,15 @@ ], "returns": "string" }, + "request_id": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "optional" + }, "status_code": { "params": [ { @@ -31035,15 +31080,6 @@ ], "returns": "class:signalwire.rest._base.HttpClient" }, - "get_project": { - "params": [ - { - "name": "self", - "kind": "self" - } - ], - "returns": "string" - }, "get_space": { "params": [ { @@ -31072,6 +31108,15 @@ } ], "returns": "class:signalwire.rest.client.RestClient" + }, + "project": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "string" } } } @@ -77345,6 +77390,15 @@ } ], "returns": "bool" + }, + "remove_xpaths": { + "params": [ + { + "name": "self", + "kind": "self" + } + ], + "returns": "list" } } } diff --git a/port_surface.json b/port_surface.json index e92818d..befe055 100644 --- a/port_surface.json +++ b/port_surface.json @@ -216,7 +216,7 @@ ] } }, - "generated_from": "signalwire-java @ d9527a22b8410bbf2f5b93bb9013f5c03a7286ee", + "generated_from": "signalwire-java @ c485c73e7ba5bbaaec4ec411d2bd52307c7714b0", "language": "java", "modules": { "signalwire": { @@ -1119,14 +1119,18 @@ "get_basic_auth_credentials", "get_basic_auth_credentials_with_source", "get_document", + "get_domain", "get_function", "get_registered_swaig_functions", "get_registered_tools", + "get_ssl_cert_path", + "get_ssl_key_path", "goto_label", "handle_request", "hangup", "has_function", "host", + "is_ssl_enabled", "join_conference", "join_room", "label", @@ -1687,6 +1691,7 @@ "__init__", "call", "control_id", + "get_completed", "get_state", "is_done", "resolve", @@ -2401,7 +2406,6 @@ "datasphere", "fabric", "get_http_client", - "get_project", "get_space", "imported_numbers", "logs", @@ -4111,7 +4115,9 @@ "get_instance_key", "get_name", "get_parameter_schema", + "get_remove_xpaths", "register_tools", + "set_remove_xpaths", "setup", "supports_multiple_instances" ] diff --git a/port_surface_native.json b/port_surface_native.json index 3cb76c2..862b863 100644 --- a/port_surface_native.json +++ b/port_surface_native.json @@ -1,5 +1,5 @@ { - "generated_from": "signalwire-java @ 4dd76a798edaad244264869da21026ab58ae680d", + "generated_from": "signalwire-java @ c485c73e7ba5bbaaec4ec411d2bd52307c7714b0", "language": "java", "modules": { "signalwire.agent.agent_base_builder": { @@ -1686,10 +1686,12 @@ "action", "await", "getCall", + "getCompleted", "getControlId", "getResult", "getState", "get_call", + "get_completed", "get_control_id", "get_result", "get_state", @@ -4860,13 +4862,17 @@ "getInstanceKey", "getName", "getParameterSchema", + "getRemoveXpaths", "get_description", "get_hints", "get_instance_key", "get_name", "get_parameter_schema", + "get_remove_xpaths", "registerTools", "register_tools", + "setRemoveXpaths", + "set_remove_xpaths", "setup", "supportsMultipleInstances", "supports_multiple_instances" @@ -5095,6 +5101,7 @@ "getBasicAuthCredentialsWithSource", "getConfigFile", "getDocument", + "getDomain", "getFunction", "getHost", "getName", @@ -5105,6 +5112,8 @@ "getSchemaPath", "getSchemaUtils", "getSecurity", + "getSslCertPath", + "getSslKeyPath", "get_all_functions", "get_auth_password", "get_auth_user", @@ -5112,6 +5121,7 @@ "get_basic_auth_credentials_with_source", "get_config_file", "get_document", + "get_domain", "get_function", "get_host", "get_name", @@ -5121,6 +5131,8 @@ "get_route", "get_schema_path", "get_security", + "get_ssl_cert_path", + "get_ssl_key_path", "gotoLabel", "goto_label", "handleRequest", @@ -5129,7 +5141,9 @@ "hasFunction", "has_function", "isSchemaValidation", + "isSslEnabled", "is_schema_validation", + "is_ssl_enabled", "joinConference", "joinRoom", "join_conference", diff --git a/src/main/java/com/signalwire/sdk/relay/Action.java b/src/main/java/com/signalwire/sdk/relay/Action.java index 06425f8..d020a61 100644 --- a/src/main/java/com/signalwire/sdk/relay/Action.java +++ b/src/main/java/com/signalwire/sdk/relay/Action.java @@ -64,6 +64,19 @@ public boolean isDone() { return done; } + /** + * Whether the action has reached its terminal state — the reference's {@code completed} flag, + * which starts false and is set true exactly once by the terminal-state resolve (call.py:90, then + * :102 inside {@code _complete}). The reference exposes BOTH this attribute and an {@code + * is_done()} method over the same state, so this port does too; {@link #isDone()} is the method + * form and reads the identical field. + * + * @return true once the action has completed. + */ + public boolean getCompleted() { + return done; + } + public void setOnCompleted(Consumer onCompleted) { // If the action has ALREADY resolved (the terminal event landed on the RELAY // reader thread before this registration — a genuine race for a caller that diff --git a/src/main/java/com/signalwire/sdk/skills/builtin/SpiderSkill.java b/src/main/java/com/signalwire/sdk/skills/builtin/SpiderSkill.java index b08a1d1..3925212 100644 --- a/src/main/java/com/signalwire/sdk/skills/builtin/SpiderSkill.java +++ b/src/main/java/com/signalwire/sdk/skills/builtin/SpiderSkill.java @@ -21,6 +21,56 @@ public class SpiderSkill implements SkillBase { // Python parity: get_instance_key defaults tool_name to SKILL_NAME (spider/skill.py). private String toolName = "spider"; + /** + * XPath expressions for elements dropped before text extraction — the reference's PREFILLED + * {@code self.remove_xpaths} default (spider/skill.py:191-199), same expressions in the same + * order. The reference drops each matching element via lxml; this port has no XPath engine on the + * scrape path, so {@link #removeXpathTagPattern()} compiles the tag names out of these + * expressions into the element-stripping regex — one source for what gets removed. + */ + private List removeXpaths = + new ArrayList<>( + List.of("//script", "//style", "//nav", "//header", "//footer", "//aside", "//noscript")); + + /** + * The XPath expressions for elements removed before text extraction. + * + * @return the removal expressions, prefilled with the reference's defaults. + */ + public List getRemoveXpaths() { + return removeXpaths; + } + + /** + * Replaces the element-removal expressions. + * + * @param removeXpaths the XPath expressions to strip before extraction. + */ + public void setRemoveXpaths(List removeXpaths) { + this.removeXpaths = removeXpaths == null ? new ArrayList<>() : new ArrayList<>(removeXpaths); + } + + /** + * Builds an alternation of the bare tag names named by {@link #removeXpaths} (a leading {@code + * //} stripped), so a simple {@code //tag} expression drives the regex strip below. An expression + * that is not a plain tag step is skipped — it cannot be honored without an XPath engine. + * + * @return the tag alternation, or null when no expression yields a usable tag. + */ + private String removeXpathTagPattern() { + List tags = new ArrayList<>(); + for (String xpath : removeXpaths) { + if (xpath == null) { + continue; + } + String tag = xpath.startsWith("//") ? xpath.substring(2) : xpath; + if (tag.matches("[A-Za-z][A-Za-z0-9]*")) { + tags.add(tag); + } + } + return tags.isEmpty() ? null : String.join("|", tags); + } + @Override public String getName() { return "spider"; @@ -43,6 +93,13 @@ public boolean setup(Map params) { this.maxTextLength = ((Number) params.get("max_text_length")).intValue(); if (params.containsKey("user_agent")) this.userAgent = (String) params.get("user_agent"); if (params.containsKey("tool_name")) this.toolName = (String) params.get("tool_name"); + if (params.get("remove_xpaths") instanceof List xpaths) { + List parsed = new ArrayList<>(); + for (Object x : xpaths) { + if (x != null) parsed.add(String.valueOf(x)); + } + this.removeXpaths = parsed; + } return true; } @@ -100,13 +157,16 @@ public List registerTools() { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); String body = response.body(); - // Basic HTML stripping - String text = - body.replaceAll("]*>[\\s\\S]*?", "") - .replaceAll("]*>[\\s\\S]*?", "") - .replaceAll("<[^>]+>", " ") - .replaceAll("\\s+", " ") - .trim(); + // Drop the removeXpaths elements (content included), then strip the + // remaining tags — the reference drops the same elements via lxml + // before calling text_content() (spider/skill.py:313-319). + String tagPattern = removeXpathTagPattern(); + if (tagPattern != null) { + body = body.replaceAll("(?is)<(" + tagPattern + ")\\b[^>]*>.*?", ""); + // Void/unclosed occurrences of the same elements. + body = body.replaceAll("(?is)<(" + tagPattern + ")\\b[^>]*/?>", ""); + } + String text = body.replaceAll("<[^>]+>", " ").replaceAll("\\s+", " ").trim(); if (text.length() > maxTextLength) { text = text.substring(0, maxTextLength) + "..."; } diff --git a/src/main/java/com/signalwire/sdk/swml/Service.java b/src/main/java/com/signalwire/sdk/swml/Service.java index 2855d76..1e7d73a 100644 --- a/src/main/java/com/signalwire/sdk/swml/Service.java +++ b/src/main/java/com/signalwire/sdk/swml/Service.java @@ -559,6 +559,55 @@ public com.signalwire.sdk.core.SecurityConfig getSecurity() { return security; } + // -------- TLS/serving values mirrored off the SecurityConfig -------- + // The reference reads these four values off `self.security` in __init__ and + // holds them directly on the service (swml_service.py:143-146), so a caller + // reaches them as `service.ssl_enabled` / `.domain` / `.ssl_cert_path` / + // `.ssl_key_path`. Java exposes the same values as accessors delegating to the + // one SecurityConfig — reading through rather than copying at construction, so + // a later `loadFromEnv()` or config reload stays reflected (the reference's + // own `start()` re-reads them the same way at swml_service.py:1240). + + /** + * Whether TLS is enabled for this service. Mirrors the reference's {@code + * SWMLService.ssl_enabled} (swml_service.py:143). + * + * @return true when TLS is on. + */ + public boolean isSslEnabled() { + return getSecurity().isSslEnabled(); + } + + /** + * The serving domain used for the TLS certificate. Mirrors the reference's {@code + * SWMLService.domain} (swml_service.py:144). + * + * @return the configured domain, or null when unset. + */ + public String getDomain() { + return getSecurity().getDomain(); + } + + /** + * Filesystem path to the TLS certificate. Mirrors the reference's {@code + * SWMLService.ssl_cert_path} (swml_service.py:145). + * + * @return the certificate path, or null when unset. + */ + public String getSslCertPath() { + return getSecurity().getSslCertPath(); + } + + /** + * Filesystem path to the TLS private key. Mirrors the reference's {@code + * SWMLService.ssl_key_path} (swml_service.py:146). + * + * @return the key path, or null when unset. + */ + public String getSslKeyPath() { + return getSecurity().getSslKeyPath(); + } + // -------- SWMLService reference-API delegators -------- // The Python reference SWMLService exposes document-manipulation and routing // helpers directly on the service; Java folds the document model into a From 659a878721607ffd637819c35cecf3e9bd013d09 Mon Sep 17 00:00:00 2001 From: Michael Jerris Date: Mon, 27 Jul 2026 13:50:45 -0400 Subject: [PATCH 05/74] test(spider): behavioural proof the removeXpaths strip loop is load-bearing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../sdk/skills/SpiderSkillTest.java | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/src/test/java/com/signalwire/sdk/skills/SpiderSkillTest.java b/src/test/java/com/signalwire/sdk/skills/SpiderSkillTest.java index 4b803d2..fde4fc7 100644 --- a/src/test/java/com/signalwire/sdk/skills/SpiderSkillTest.java +++ b/src/test/java/com/signalwire/sdk/skills/SpiderSkillTest.java @@ -3,7 +3,14 @@ import static org.junit.jupiter.api.Assertions.*; import com.signalwire.sdk.skills.builtin.SpiderSkill; +import com.signalwire.sdk.swaig.FunctionResult; import com.signalwire.sdk.swaig.ToolDefinition; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import java.util.*; import org.junit.jupiter.api.Test; @@ -53,4 +60,158 @@ void testToolsHaveDescriptions() { assertFalse(td.getDescription().isEmpty()); } } + + // ======== remove_xpaths (reference spider/skill.py:191-199, :313-319) ======== + // The reference drops SEVEN element subtrees via lxml drop_tree before + // calling text_content(). Anything it does not drop reaches the LLM as + // "scraped content", so the set is a behavioural contract, not decoration. + + private static final List REFERENCE_REMOVE_XPATHS = + List.of("//script", "//style", "//nav", "//header", "//footer", "//aside", "//noscript"); + + @Test + void testRemoveXpathsIsPrefilledWithTheReferenceDefaults() { + assertEquals(REFERENCE_REMOVE_XPATHS, new SpiderSkill().getRemoveXpaths()); + } + + @Test + void testSetupOverridesRemoveXpaths() { + SpiderSkill skill = new SpiderSkill(); + skill.setup(Map.of("remove_xpaths", List.of("//aside"))); + assertEquals(List.of("//aside"), skill.getRemoveXpaths()); + } + + /** + * The load-bearing test. Every element the reference drops must have its TEXT CONTENT absent from + * the scraped output — not just its tag stripped. A port that only flattens tags leaks + * nav/header/footer/aside/noscript prose into the model's context. + */ + @Test + void testScrapeDropsEveryRemovedElementSubtree() throws Exception { + String html = + "" + + "" + + "
HEADERTEXT
" + + "" + + "" + // Deliberately NOT "NOSCRIPTTEXT": that string CONTAINS "SCRIPTTEXT", + // so a noscript leak would masquerade as a script leak and the two + // element types could not be told apart. + + "" + + "

KEEPTEXT

" + + "
FOOTERTEXT
" + + ""; + try (PageServer page = PageServer.serving(html)) { + String out = scrape(new SpiderSkill(), page.url()); + + assertTrue(out.contains("KEEPTEXT"), "real page prose must survive: " + out); + for (String leaked : + List.of( + "SCRIPTTEXT", + "STYLETEXT", + "NAVTEXT", + "HEADERTEXT", + "FOOTERTEXT", + "ASIDETEXT", + "NOSCRIPTONLYTEXT")) { + assertFalse( + out.contains(leaked), + "removed element's text content leaked into scraped output: " + leaked + " in " + out); + } + // alert(1)'s body is inside