Files
wip-rs/MIGRATION.md
Hare 8ecc307fc9 feat: migrate protocol and HTTP to structured interface references
T-20: identify interfaces by exact scope/name pairs; add optional opaque scope_ref metadata and separate shape validation from path-dependent scope compatibility. Preserve call error precedence, strict JSON, validators and transport boundaries. Mechanically migrate consumers and document API/wire compatibility and deferred lifecycle semantics.

Validation: cargo fmt --all --check; git diff --check; cargo check --workspace --all-targets; cargo test -p wip-protocol -p wip-http; cargo test --workspace (221 passing tests including doctests); cargo test --workspace --all-targets; nix develop --command cargo clippy --workspace --all-targets -- -D warnings; RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps. All pass. cargo/rustc 1.95.0; development-shell Clippy/rustc 1.97.1.
2026-10-07 18:29:51 +09:00

5.8 KiB

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<InterfaceReference>. FetchInterfaceRequest.interface, FetchInterfaceResponse.interface, and InterfaceTarget.reference all use the same record:

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<String> 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:

{"scope":"/github","name":"issue.read"}

For example:

{
  "interface": {"scope":"/github","name":"issue.read"},
  "scope_ref": "opaque-scope-object-ref",
  "descriptor": {"format":"wip-interface/1","types":[],"operations":[]}
}

A call uses:

{
  "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.