Files
wip-rs/MIGRATION.md

250 lines
13 KiB
Markdown

# Structured Interface reference and scope publication migration
## Transport-independent Client API
The default `wip-client` feature set still exposes the HTTP-compatible
`wip_client::Client` and `PreparedRequest`. Its constructor takes state and wire
limits, sessions canonicalize HTTP endpoints, and `complete` consumes an HTTP
response. Existing TUI and native HTTP consumers need no source migration; the
wire contract is unchanged.
For an in-process Host (including Client and Host in one WASM module), disable
default features and use `wip_client::direct::{Client, Request, Response}`:
```toml
wip-client = { version = "0.2.0", default-features = false }
```
- `direct::Client::new(ClientLimits)` takes no wire limits.
- `open_session(worldspace_identity, SecurityContext)` accepts an opaque identity,
not a URL. Issue a separate identity per Worldspace view/Host lifetime and a
separate security identity whenever authorization context changes. Direct and
canonical HTTP session identities cannot alias each other's cache.
- Preparation methods retain their names and arguments. Their result is
`direct::PreparedRequest { id, request: Request }`, with `Observe`,
`FetchInterface`, and `CallOperation` variants containing Protocol request DTOs.
- Route that exact DTO to the session's Host/security context; mark calls
dispatched before invoking the Host. Complete with the original `id` and the
matching `Response` variant containing `Result<Protocol DTO, ProtocolError>`.
No HTTP request, response, JSON codec, or artificial endpoint is needed.
- Direct responses undergo the same semantic validation and state transitions
as HTTP responses. Wrong interactions and invalid results are rejected;
invalid call responses retain an unknown execution outcome. Retrievals may
be explicitly refreshed, but operations are never automatically retried.
- `ClientLimits` still bounds cache, sessions, in-flight requests, and history.
HTTP status/envelope checks and byte/JSON nesting limits belong to the optional
HTTP adapter. An embedding application owns DTO allocation/execution budgets.
The `direct` module API documentation contains a runnable typed example. Run
`cargo test -p wip-client --no-default-features` to exercise the core without an
HTTP dependency; ordinary `cargo test -p wip-client` also runs unchanged HTTP
regressions and direct/HTTP state parity tests.
The 0.2.0 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:
```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<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.
Host enforces scope publication lifetimes and optional scope-ref identity
inside the provider boundary. Client caches keep the complete structured reference
and optional fetched scope binding; confirmed scope deletion or ref replacement
invalidates bound observations, while local eviction is not deletion evidence.
Do not mistake registry artifact retention for publication.
## 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.
## Host registration and provider migration
Registration now requires an explicit canonical Scope and local name:
```rust,ignore
let mut registry = wip_host::InterfaceRegistry::new();
let scope = wip_host::Scope::new("/service")?;
let api = registry.register(&scope, "catalog", CatalogDefinition, None)?;
// INSIDE the backend transaction/lock/stable-handle boundary:
let current = wip_host::ScopeObservation::new(&scope, &current_scope_object);
let publication = wip_host::PublishedObject::new(current_target_object)
.expose(&api)?
.bind_scope(current);
```
Collision keys are exact `(scope, name)` coordinates, not Rust Definition TypeId.
The same Definition can be registered in different scopes or under different
names. Root, self and ancestor scopes are valid, but ancestor compatibility and
explicit target membership remain independent checks. Descriptors still have
no publication identity fields.
A Scope carries a private publication lifetime, not a generated Protocol ref.
Clone it only while the same scope Object remains published: ordinary Object
state/validator updates do not retire it. Deletion, replacement or movement
requires retiring the old lifetime with `registry.remove_scope(&scope)` and
creating a fresh Scope before republication, even at the same path or ref.
Retained registration tokens cannot fetch or start calls after retirement.
Ref-less replacement cannot be tracked by a Client identity precondition; refs
and scope_ref remain optional, not forcibly generated.
`ScopeObservation::new(&scope, &object)` is a provider attestation that this
lifetime currently belongs to the Object at the scope path. Construct and attach
it with `PublishedObject::bind_scope` inside the same coherent boundary as target
resolution, membership, Descriptor/validator selection, implementation and
backend handles, through dispatch and commit/rollback. A matching ref or an
independent scope prelookup is not sufficient. The scope Object need not expose
any interfaces. Providers own atomicity and external-writer limitations; the
runtime checks the attached lifetime and optional scope_ref, and validates both
arguments and results against the same frozen Descriptor. A supplied scope_ref
mismatch, including a missing current ref, is `InterfaceMismatch` at membership
precedence, after target existence and a supplied Object validator mismatch.
Omission applies no scope identity precondition.
Artifact-only `registry.fetch_interface(reference)` is removed. Prefer
`ObjectProvider::fetch_interface(&provider, &registry, &reference)`, which resolves
the visible scope Object with `with_object` and uses its attached current binding.
Low-level `registry.fetch_interface(&reference, &current_observation)` requires
the same coherent current attestation, not a cached snapshot. Fetch returns
Descriptor, Interface validator and optional scope Object ref together; Clients
need not additionally fetch an ancestor Object. `registry.scopes()` may repeat
a handle for several names; do not treat it as a unique-scope iterator.
`PublishedObject::protocol_object()` is a path-free composition projection, not
publication authority. For current observations use `ObjectProvider::observe_object`
or `publication.protocol_object_at(path)` inside the coherent provider boundary;
the latter also validates compatible live scope associations and response shape.
## Downstream status
The filesystem default registers local names `file.read`, `symlink.inspect` and
`file.write` at its deployment-pinned root Scope, displayed as `/::file.read` etc.
External compositions choose explicit Scope/local names and implement
`Composition::scope` to attest a non-root lifetime from the exact refreshed backend
entry in its connected lineage. The engine never independently resolves a scope
path during dispatch. Default composition remains root-scoped; exposing non-root
registrations requires the application's current lifetime attestation, not merely
an artifact or matching ref/validator. All bindings are formed inside backend
execution, with current principal lineage visibility and publication metadata.
Retire/recreate Scope handles on deletion, replacement or movement; a pinned leaf
alone is not evidence that a self-scope Object remains published at that path.
The Linux deployment root remains pinned across textual root rename/replacement,
and its ref-less default publication continues to omit scope_ref.
The public API example self-scopes `catalog`, `audit` and `diagnostics` at
`/article-service`, fetches through its provider, and constructs call metadata
from the fetched structured reference and scope_ref. External-style conformance
tests also cross real TCP HTTP for root/self-scopes, same names in different
scopes, optional refs, stale refs, deletion/republication and validator precedence.
The TUI stores structured references in cache keys, selections, actions, editors
and in-flight subjects. Normal screens reuse the canonical absolute scoped
formatter (`/scope::name#operation`, with JSON component quoting when needed).
Labels are never parsed to reconstruct identity, and scope_ref/validators are not
normal-screen reference labels. Client invalidation is synchronized back into UI
observations before dispatch; re-observation is not automatic execution. Typed
result entries are still fetched on selection, not all at once, and do not create
inferred indexable edges. Interface authoring/macros remain descriptor-only.