# Structured Interface reference migration This unreleased API/wire change follows `wip-reference` canonical Core Model, Protocol and WIP over HTTP at `6086f3c5ef10aa1464ce9c824750d7f667217bfd`. It replaces the earlier opaque Interface reference contract; HTTP routes and error codes have **not** changed. Peers must upgrade together: the codec does not accept legacy strings or display shorthand as Protocol references. ## Rust API `Object.interfaces` is now `Vec`. `FetchInterfaceRequest.interface`, `FetchInterfaceResponse.interface`, and `InterfaceTarget.reference` all use the same record: ```rust use wip_protocol::{FetchInterfaceRequest, InterfaceReference, InterfaceTarget}; let reference = InterfaceReference { scope: "/github".into(), name: "issue.read".into(), }; reference.validate()?; // reference shape, no target Object path needed let fetch = FetchInterfaceRequest { interface: reference.clone() }; let target = InterfaceTarget { reference, scope_ref: Some("opaque-scope-object-ref".into()), validator: None, }; # Ok::<(), wip_protocol::ValidationError>(()) ``` Construct scope and name explicitly from your Worldspace publication context. There is no implicit conversion from a string, delimiter parser, normalization, case folding, or URL decoding. A name may contain `/`, `.`, `::`, `#`, or Unicode; only an empty name is invalid. Scope must be a canonical absolute Worldspace path: `/`, or nonempty slash-separated segments other than `.` and `..`, with no empty segments or trailing slash. `%2F` is ordinary field text, not an escape for a Worldspace path separator. Equality, hashing and ordering operate on both fields, independent of member order/JSON escape spelling. Use the record itself as a membership/cache key; identical names in different scopes are different references. The descriptor still contains only format, documentation, types and operations, not identity. `FetchInterfaceResponse` and `InterfaceTarget` additionally require the Rust field `scope_ref: Option` in struct literals. Use `None` when omitted, not an invented identity. This is an opaque scope Object ref observation/call precondition, **not** a third Interface identity component or a validator. The scope-ref lifecycle/atomic Host enforcement and Client cache-recovery semantics are follow-up work (T-21/T-22); this migration supplies the data model and codec, not a completed lifecycle implementation. ## Validation boundaries - `InterfaceReference::validate()` and `FetchInterfaceRequest::validate()` check reference shape independently of a target Object path. - `Object::validate()` checks shape and duplicate `(scope, name)` pairs; `Object::validate_at_path()` additionally checks placement, including scope. `ObserveRequest::validate_response()` checks every derived node path, including the depth boundary. Invalid success responses are Client-local `InvalidResponse`, not Host-returned Protocol errors. - `InterfaceReference::validate_for_path(path)` checks ancestor-or-self scope compatibility by path segments. Root and self scope are valid; `/a` is not an ancestor of `/ab`. This has no indexability dependency. - `CallOperationRequest::validate()` checks descriptor-independent shape. `validate_with()` adds operation/argument checks but does not replace Host publication lookup. Host interface selection checks scope compatibility and exact membership **after** target lookup: invalid reference shape is `InvalidRequest`, but a valid reference outside the target's scope is `InterfaceMismatch`, with `NotFound` retaining precedence. ## HTTP wire All four reference positions use an exact object with required string fields: ```json {"scope":"/github","name":"issue.read"} ``` For example: ```json { "interface": {"scope":"/github","name":"issue.read"}, "scope_ref": "opaque-scope-object-ref", "descriptor": {"format":"wip-interface/1","types":[],"operations":[]} } ``` A call uses: ```json { "target": {"path":"/github/issues/123"}, "interface": { "reference": {"scope":"/github","name":"issue.read"}, "scope_ref": "opaque-scope-object-ref" }, "operation":"refresh", "arguments":{} } ``` Unknown/duplicate members, missing fields, null fields, relative scopes, empty names, legacy `"interface-v1"` and shorthand `"/github::issue.read"` strings are rejected. Field order and JSON escaping do not affect decoded identity. `scope_ref` is a normal opaque string, **not Base64**; omission is allowed but explicit null and nonstrings are invalid. An empty string is a valid opaque ref. Validators remain canonical padded standard Base64. Descriptor-independent HTTP call metadata decoding enforces reference shape and the same strict JSON rules without turning scope compatibility into early `InvalidRequest`. Normal descriptor-bound encode/decode uses the same reference codec. Request violations retain the existing `400 invalid_request` boundary; malformed success responses retain Client-local `InvalidResponse`. Standard `POST v1/observe`, `v1/fetch_interface`, `v1/call_operation`, error-code/status mapping and Transport failure classification are unchanged. ## Downstream status The workspace Client, Host, filesystem server, TUI and public-API example are mechanically migrated to structured protocol values and updated fixtures. The existing Host's generated opaque registration handle is bridged explicitly to scope `/` with its full handle as the name; no delimiter parsing or scope registration/lifetime model is introduced here. The existing Client keys use structured values; this change does not implement scope deletion/replacement cache recovery. TUI formatting is only the type-compatibility adaptation, not the full canonical Text View work (T-24). Filesystem/TUI semantics integration remains T-23. Interface authoring/macros are descriptor-only and require no identity fields or declaration changes.