Files

158 lines
8.5 KiB
Markdown

# Public-document Worldspace
`wip-site-worldspace` is the site's in-memory Host provider: immutable documents
plus an opt-in session-local playground. It uses
`wip-host`, `wip-interface` and `wip-protocol`; it does not depend on the filesystem
server, HTTP, a browser, or a network runtime. The native and WASM callers use the
same `Worldspace` instance API.
## Generated data
The static Astro endpoint `src/pages/worldspace.json.ts` emits
`dist/worldspace.json` during the normal site build. It:
1. Reads `documents()` (the existing public-only content collection).
2. Renders each document with Astro's existing Markdown renderer, including the
configured document-link transform and syntax highlighting.
3. Renders the existing `RootPage` and `DirectoryIndex` components for `/`, `/book`
and any real public directory indexes. It does not copy their authored text.
4. Passes rendered bodies to `createWorldspaceData` in
`src/lib/worldspace-data.ts`, which uses `bookTree` and filters drafts again.
The input contract is:
```json
{"nodes":[
{"path":"/","name":"","displayPath":"/","title":"Worldspace Interface Protocol","href":"/","html":"..."},
{"path":"/book","name":"book","displayPath":"/book","parent":"/","title":"Book","href":"/book/","html":"..."},
{"path":"/book/1-concept.md","name":"1-concept.md","displayPath":"/1-concept.md","parent":"/book","title":"...","href":"/book/1-concept/","html":"..."},
{"path":"/book/1-concept.md/1.1-interaction-abstraction.md","name":"1.1-interaction-abstraction.md","displayPath":"/1.1-interaction-abstraction.md","parent":"/book/1-concept.md","title":"...","href":"/book/1.1-interaction-abstraction/","html":"..."}
]}
```
`path` is the only WIP request identity. `parent` is a WIP path and is absent only
for `/`. `displayPath` preserves the old explorer identity and selection state;
`href` preserves the existing Web route (including `SITE_BASE`). The UI can keep
its current `book/` label, but the actual Object name is `book`. No protocol name
has a trailing slash. Nodes retain `bookTree` order. The provider does not sort
children lexically. Every node, including home, book index, and a chapter that
also has sections, has a readable body.
`html` is a trusted, build-rendered fragment, not a full page or an untrusted user
submission. Consumers must retain the site's existing CSS, including home scoped
styles and code-block themes. Do not inject raw JSON into a script element without
escaping `<`; the generated endpoint is served as `application/json` instead.
There is no runtime loading by path: unpublished or guessed draft paths cannot
reach a source file. The Rust loader also rejects draft path segments, duplicate
path/displayPath/URL identities, inconsistent parent/name/path relationships,
missing parents, malformed input and unknown JSON fields.
## Rust API
```rust
use wip_site_worldspace::{Worldspace, DocumentContent};
use wip_site_worldspace::wip_protocol::{
ObserveRequest, FetchInterfaceRequest, CallOperationRequest, Target, InterfaceTarget,
};
use wip_interface::WipType;
# fn example(json: &str) {
let world = Worldspace::from_json(json).unwrap();
let observed = world.observe(ObserveRequest { path: "/".into(), depth: 4 }).unwrap();
let reference = observed.object.interfaces[0].clone();
let interface = world.fetch_interface(FetchInterfaceRequest { interface: reference.clone() }).unwrap();
let result = world.call_operation(CallOperationRequest {
target: Target { path: "/book/1-concept.md".into(), validator: None },
interface: InterfaceTarget {
reference, scope_ref: interface.scope_ref, validator: interface.validator,
},
operation: "read".into(),
arguments: Default::default(),
}).unwrap();
let document = DocumentContent::decode(result.result).unwrap();
assert_eq!(document.path, "/book/1-concept.md");
# }
```
Alternatively use `Worldspace::new(WorldspaceData { nodes })`. Construction returns
`Result<Worldspace, DataError>`. `observe(ObserveRequest)`,
`fetch_interface(FetchInterfaceRequest)` and `call_operation(CallOperationRequest)`
take requests by value and return canonical protocol response types or
`ProtocolError`. The crate re-exports `wip_protocol` for bridge consumers.
The registered root-scoped Interface is `{scope:"/",name:"document"}`. Its only
Operation, `read`, takes **no arguments** and returns the typed `DocumentContent`
record `{path,title,href,html}`. Host-generated Descriptors, scope association,
interface membership, request validation, argument/result validation and error
classification remain on the normal Host path. The provider exposes no alternate
body-fetch method. Documents are immutable, so no validators are advertised.
The recursive adapter calls `ObjectProvider::observe_object` for every node and
validates the assembled response against the request. At the requested depth,
`children` is `None`; above it, a visited leaf has `Some([])`. Unknown paths return
`NotFound` and malformed paths return `InvalidRequest`. Requests above
`MAX_DEPTH = 32` return `ResourceLimitExceeded` rather than silently truncating.
Construction allows at most `MAX_NODES = 4096`, with hierarchy depth at most 32.
`from_json` also caps input at 16 MiB. Together these bounds cap traversal work.
## Playground
A manifest document at `/playground` opts into mutable state. Manifests without
that node retain document-only behavior. Manifest descendants of `/playground`
are rejected, so mutable counters cannot shadow or delete document content.
- `/playground` keeps `/::document.read()` and also exposes `/::collection`:
`create(name: String) -> Unit` creates a zero-valued counter;
`remove(name: String) -> Unit` removes an existing counter.
- Children start as `apple = 3` and `orange = 0`, ordered lexically. Each exposes
only `/::counter`: `get() -> Integer`, `increment(amount: Integer) -> Integer`,
and `reset() -> Integer`. Increment returns the new value; reset returns zero.
- Names match `[A-Za-z0-9_-]{1,32}` and are case-sensitive. At most 32 counters
exist. Duplicate creation, missing removal, invalid names, and checked sums
outside Protocol Integer's range `[-9007199254740991, 9007199254740991]` fail
without mutation. Capacity errors are `ResourceLimitExceeded`; domain
rejections are `Internal` with an explicit not-committed Host guarantee.
Invalid typed arguments are rejected by standard Client/Host validation.
- Every counter lookup publishes a fresh Object snapshot whose `description`
is the current decimal integer and whose `name` remains the slug. All
generated interfaces bind to the same actual root ScopeObservation.
- State belongs to one Worldspace/BrowserSession. Navigation retains it; a new
session restores initial state. Documents are never mutated.
The browser's normal `tree()` includes only manifest display paths and therefore
never calls `document.read` on dynamic counters. Manual playground `observe`
uses the real Client's `refresh_observed` on every explicit retrieval (including
recovery from retained missing/stale observations); other documents retain
`ensure_observed` caching. This Client version has no public subtree-invalidation
API. A call does not observe, fetch, retry, or convert its result into an Object.
The UI explicitly observes `/playground` at depth 1, then the selected Object at
depth 1 after calls, and observes `/playground` on entry to Protocol mode.
## Validation
From the site directory, with the configured Rust toolchain and dependencies:
```sh
node --test tests/worldspace-data.test.mjs
cargo test -p wip-site-worldspace
npm run build
WIP_TEST_GENERATED=1 node --test tests/worldspace-data.test.mjs
cargo test -p wip-site-worldspace --test worldspace -- --include-ignored
npm run check:links
```
The explicit generated-data tests require `dist/worldspace.json`; they do not
silently substitute fixtures when the artifact is absent. Native fixture tests
exercise all three interactions, depth boundaries/order/empty sets, limits,
identity and constructor errors, unknown paths/interfaces/operations, invalid
arguments, validator failures, draft guesses, and JSON parsing. The generated
native test invokes all three interactions against the actual build artifact.
The JS post-build test compares every returned body and link set with the static
page's article, including home and book index. Run it again with a prefixed
`SITE_BASE` build to check deployment URLs.
Cargo dependency sourcing and the browser bridge are managed by the parent site
workspace. The initial relative Rust dependencies require the companion `wip-rs`
checkout beside `docs`; they must be replaced with the final reproducible source
configuration before a standalone site checkout can build.