Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "uhura-dev",
"runtimeExecutable": "uhura/target/debug/uhura",
"runtimeArgs": ["dev", "examples/instagram-uhura", "--port", "8787"],
"port": 8787
}
]
}
43 changes: 43 additions & 0 deletions .github/workflows/uhura.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Uhura spike CI — path-filtered; the root ci.yml is untouched.
# Steps grow with milestones (design §12.5); M0 ships fmt/clippy/test.
name: uhura

on:
push:
paths: ["uhura/**", ".github/workflows/uhura.yml"]
pull_request:
paths: ["uhura/**", ".github/workflows/uhura.yml"]

defaults:
run:
working-directory: uhura

jobs:
uhura:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# rustup honors uhura/rust-toolchain.toml automatically.
- uses: Swatinem/rust-cache@v2
with:
workspaces: uhura
# --locked everywhere: the committed Cargo.lock IS the pin.
- run: cargo fmt --all --check
- run: cargo clippy --locked --workspace --all-targets -- -D warnings
- run: cargo test --locked --workspace
# The corpus lives at the repo-root examples/ (../ from working-directory).
- run: cargo run --locked -p uhura-cli -- fmt --check ../examples/instagram-uhura
- run: cargo run --locked -p uhura-cli -- check ../examples/instagram-uhura --deny-warnings
# M4: the headless demo walkthrough runs end to end (goldens live in
# uhura-tests; this is the CLI-level smoke).
- run: cargo run --locked -p uhura-cli -- trace ../examples/instagram-uhura --script=demo > /dev/null
- run: cargo run --locked -p uhura-cli -- project ../examples/instagram-uhura --out=renders
- uses: actions/upload-artifact@v4
with:
name: uhura-canvas
path: uhura/renders/canvas.html
# M5: the wasm crate compiles for its real target (bindgen + parity
# land with M6's full battery); the play shell typechecks strict.
- run: cargo build --locked -p uhura-wasm --target wasm32-unknown-unknown
- run: npx -y -p typescript@5.6.3 tsc -p shell/jsconfig.json
# M6: ./scripts/build-wasm.sh && node scripts/parity.mjs && acceptance
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
**/*.rs.bk
.DS_Store

# Uhura corpus build artifacts (nested workspace corpus at repo-root examples/)
/examples/instagram-uhura/build

# Prebuilt binaries assembled into the npm package at release time (RFD 0020).
/npm/binaries/
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,11 +626,24 @@ prints the SDL for offline schema tooling.
The npm package metadata lives under `npm/` only to reserve the package name.
It is not the primary implementation target.

## Uhura, the sibling client language

This repository also hosts [Uhura](uhura/README.md) — an incubating
declarative UI language and deterministic headless experience runtime, the
client-side sibling of Spock. Spock deliberately borrows its client tooling
(`docs/rfd/0010`) and never grows a client language of its own; Uhura fills
that slot as a separate language with its own nested Cargo workspace under
`uhura/`, with Spock as its canonical backend provider. The integration
direction — contract projection, adapter obligations, and the determinism
boundary — is recorded in `docs/rfd/0022-uhura-the-client-language.md`.

## Repository Layout

- `examples/` contains product requirements and current-valid Spock examples.
- `docs/rfd/` contains discussion drafts and proposal-only language ideas.
- `npm/` contains package metadata for npm name reservation.
- `uhura/` contains the Uhura client language and runtime — a sibling
project in its own nested Cargo workspace (RFD 0022).

## References and prior work

Expand Down
173 changes: 173 additions & 0 deletions docs/rfd/0022-uhura-the-client-language.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# RFD 0022 — Uhura, the client language beside Spock

Status: **discussion draft**. This RFD records why the [Uhura](../../uhura/README.md)
project now lives in this repository, what the integration between the two
languages will look like, and where the hard seam is. It lands no code: the
contract projection and the adapter it describes are direction, not
implementation. The type-mapping decisions in §4 are recorded as open, not
decided.

## 1. What Uhura is

Uhura is an incubating declarative UI language and deterministic headless
experience runtime — "a minimal Svelte without JavaScript." Svelte-flavored
markup over a closed transition language and typed service ports, compiled by
a Rust checker into a replayable machine that evaluates a checked program into
a renderer-neutral semantic view and emits typed commands and platform
intents. It owns UI-session state and experience behavior; it does not paint
pixels, perform I/O, or hold authoritative product truth. The authoritative
design lives in `uhura/docs/`, exercised end to end by the Instagram slice at
the repo-root `examples/instagram-uhura/`.

## 2. Why it lives here — the vacant client slot

Spock's own doctrine forbids Spock from building a client language. RFD 0010's
governing rule is "generate types, never the client"; RFD 0009's walls are
"borrow, don't build" and "language work is the differentiator" — where the
language is Spock itself. Everything client-side is deliberately borrowed:
graphql-codegen, urql, Apollo, a future thin protocol client. The one thing
that doctrine rules out of Spock is exactly the space a *sibling* language
occupies: a specification language for the client half of the product, with
its own grammar, checker, and runtime.

The fit was designed in from both sides. Uhura's design doc names Spock as its
intended real provider — its architecture diagram reads `fixture driver ⇄
future Spock adapter`, its seam crate is documented as "the Spock seam crate,"
and its §9.6 is titled "The Spock-replaceability argument," pinning exactly
what stays byte-identical when Spock replaces the test fixture. Together the
two languages state the whole product: Spock is the executable PRD for the
backend; Uhura is the executable PRD for the experience. Spock's README notes
that commercial prototyping tools stop at click-through screens — "the backend
of every prototype is imaginary." Uhura is the inverse image: a real client
prototype whose backend has so far been a scripted fixture. Each is the
other's missing half, and the shared Instagram dogfood makes that literal
(§6).

**The relationship is canonical provider, not hard-wiring.** Uhura's port seam
(`uhura-provider/0` envelopes, hash-pinned port contracts, a conformance suite
that runs against any driver) stays provider-neutral, and the scripted fixture
remains the permanent CI test double — both per uhura design §9.6. Spock
becomes the privileged first provider: dogfooded, contract-projected, and what
`uhura.lock` binds to by default. This mirrors Spock's own relationship to
SQLite — first conforming engine, not the definition.

Boundary hygiene, restated from both projects' founding docs: no fact may be
authoritative in both languages; Uhura is a separate nested Cargo workspace
with its own toolchain pin and no dependency on Spock internals; the `spock`
npm name and binary surface are untouched. Uhura sits beside the reserved
slots, never inside them.

## 3. What binds them — the contract, not the crates

Direct code sharing between the two workspaces is deliberately near-zero. The
value models are doctrinally incompatible: Spock has `float` and wall-clock
`timestamp`/UUIDv7 throughout; Uhura's core denies float arithmetic at the
compiler level, bans clocks and randomness from the machine, and its
canonical-JSON hasher rejects floats outright. Forcing a shared base crate
would make one language lie about its doctrine. What is genuinely shared is
one level up: **the contract as data**.

Spock already emits its whole contract as a frozen-additive JSON document
(`GET /~contract`, spec §6) that downstream tools consume without touching the
compiler. Uhura already consumes typed port contracts (`*.port.toml`,
hash-pinned) from whoever provides them. The integration artifact is a
projection from the first to the second.

## 4. The contract projection

A deterministic generator — working name `spock gen ports` (binary-owned, per
RFD 0010's "the binary owns generation"), or equivalently `uhura import spock`
on the other side; which binary owns it is an open decision — that reads the
contract JSON and emits Uhura port contracts:

| Spock (contract IR) | Uhura (port contract) |
| --------------------------------------- | -------------------------------------------- |
| `fn` (mut) | command with outcome union |
| minted refusals (`!` codes, 409) | refusal variants of the outcome |
| derived error codes (per constraint) | typed failure outcomes |
| reserved codes / transport failure | `unavailable` (adapter-mapped, §5) |
| read `fn` / future `view` | read-model projection |
| `record` (flat, scalar-only) | port record |
| `set` (closed string union) | `enum` |
| `uuid` | `id` |
| `text`, `int`, `bool` | `text`, `int`, `bool` |

Two type decisions are recorded here and deliberately left open:

- **`timestamp`** — Uhura has no datetime type and forbids clocks. Candidates:
project as an opaque nominal (echo-only, ordered server-side), or exclude
from port shapes and require contracts to expose ordering some other way.
- **`float`** — excluded from ports. Uhura cannot carry it; a contract field
that a client genuinely needs must be reshaped (fixed-point int, text) on
the Spock side.

The projection must be total and checked: a Spock contract that cannot be
projected should fail generation with a stated reason, never emit a port
contract that lies (the same law RFD 0010 sets for the TypeScript emission).

## 5. The adapter

A real provider adapter — the thing uhura's design calls the "future Spock
adapter" — owns everything the port seam forbids from crossing into the
machine:

- **Wall-clock and transport.** The machine has no timeouts; the adapter maps
transport failure to `unavailable`.
- **Identity.** The `X-Spock-Actor` header (RFD 0014), later the bearer token,
lives entirely in the adapter. Nothing identity-shaped crosses the seam.
- **Settlement.** Uhura requires exactly-one-outcome-eventually and read-model
consequences settled at or before the outcome. Spock satisfies the hard
half for free: fns return the written row, which is precisely the
"read-model fragment carried in the command response" pattern uhura design
§9.4 names as the conforming CQRS shape.
- **Conformance.** The adapter passes the same conformance suite the fixture
passes. The fixture stays the CI double; the adapter is for live play.

## 6. The determinism boundary

The one deep tension. Spock mints wall-clock UUIDv7 ids and real UTC
timestamps on every write — both on Uhura's forbidden-inputs list. Against a
live Spock, Uhura's *per-step* determinism is untouched (that property never
depended on the provider), but *byte-golden replay* of full traces is not
possible: two identical runs produce different ids and timestamps.

Uhura's design already absorbs this — golden traces live on the fixture,
live delivery order is allowed to be nondeterministic — so nothing breaks. But
if end-to-end golden traces across both languages are ever wanted, the
resolution is on the Spock side: a deterministic prototype mode (seeded id
mint, virtual clock). That fits Spock's identity — the README already imagines
sagas running in simulated time — but it is a real RFD of its own and is
explicitly out of scope here.

## 7. Acceptance direction

Both repos dogfooded the same product. Spock's `examples/instagram/v0.spock`
models 19 tables and 43 fns; its TypeScript client demo exercises five floor
CRUD calls and zero deliberate fns — no actor header, no refusal branching, no
pagination (`v0-FEEDBACK.md`, G12/G13/G16 and the client README's own scope
note). Uhura's Instagram spike is exactly the missing consumer: optimistic
like with rollback, typed refusal branching, cursor pagination, keyed
navigation.

The integration's acceptance proof is therefore already written: **Uhura's
Instagram slice running against Spock's `v0.spock` through the projection and
the adapter.** The day that works, the client-surface gaps in Spock's own
dogfood ledger close from the outside, and the two executable PRDs compose
into one.

The two slices sit side by side today as distinct repo-root folders —
`examples/instagram/` (Spock backend) and `examples/instagram-uhura/` (Uhura
client) — sharing only the canonical PRD, because they are not yet wired.
Merging them into a single `examples/instagram/` domain (backend + client
sides) is deferred to the milestone that lands the projection and adapter,
when the runtime wire dictates the shared layout.

## 8. Non-goals

- No code lands with this RFD — projection and adapter are future milestones.
- No npm name reservation for uhura; it publishes nothing.
- The `UHURA_REQUIRE_PARITY=1` CI gate stays deferred (uhura's own debt
ledger).
- No repository rename is proposed.
- No change to Spock's language surface, roadmap order (RFD 0009), or the
reserved `spock` npm client slot (RFD 0010).
11 changes: 11 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ An example should:
If an example needs a language feature that does not exist yet, write an RFD
instead of adding the feature here.

## Shared with Uhura

A domain's `PRD.md` is language-neutral, so the same PRD can be dogfooded from
both ends. The Instagram PRD is also the canonical spec for the Uhura client
slice in the sibling folder [`instagram-uhura/`](instagram-uhura/README.md):
Spock implements the backend here, Uhura the experience there. The two are
kept as distinct folders until the languages are actually wired, then merged
into one `instagram/` domain — see
[RFD 0022](../docs/rfd/0022-uhura-the-client-language.md) for the integration
direction.

## Answer Sheets (`pg.sql`, …)

A scenario may also include a reference implementation in a conventional
Expand Down
34 changes: 34 additions & 0 deletions examples/instagram-uhura/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Instagram — Uhura client slice

This is the Uhura implementation of the **client** half of the Instagram
example: the feed, profile, post, like, comment, and pagination experience,
compiled into a deterministic headless machine and driven by the scripted
fixture under `fixtures/`.

It is built and played from the sibling [`uhura/`](../../uhura/) workspace
(paths are `../examples/instagram-uhura` from there — see that README's quick
tour).

## Relationship to `examples/instagram/`

The Spock backend slice for the same product lives next door in
[`examples/instagram/`](../instagram/). They are kept as **two distinct
folders for now** because the two languages are not yet wired together — Uhura
still runs against its scripted fixture, not a live Spock provider. Once the
integration in [RFD 0022](../../docs/rfd/0022-uhura-the-client-language.md)
lands (contract projection + adapter), the plan is to **merge these into one
`examples/instagram/` domain** with a backend and a client side.

The product requirements are already shared: the canonical, language-neutral
PRD is [`examples/instagram/PRD.md`](../instagram/PRD.md), which Spock's
`v0.spock` implements and this slice targets. Uhura's assets are locally
generated (no shared media).

## Layout

- `app/` — routes and pages (`.uhura`)
- `components/` — reusable markup + `store` machines
- `ports/` — the typed service seam (`*.port.toml`) a Spock provider would satisfy
- `fixtures/` — scripted provider data + play scripts (the CI test double)
- `catalog/`, `styles/`, `surfaces/` — icon/token catalog, CSS, surface defs
- `uhura.toml` / `uhura.lock` — app manifest and port-binding lock
66 changes: 66 additions & 0 deletions examples/instagram-uhura/app/feed/page.examples.uhura
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use fixture standard

example loading {
note "cold start — nothing delivered yet"
}

example first-page default {
projection feed.viewer = fixture.users.mira
projection feed.feed-page = fixture.feed.page-1
}

example like-pending {
from first-page
events [ like-toggled(post: "post-lena-glaze", now-liked: true) ]
note "optimistic heart + count while like-post is in flight"
}

example like-refused {
from like-pending
events [ outcome like-post.err(refusal: rate-limited) ]
note "authority refused — rollback, notice explains"
}

example comments-open {
from first-page
projection comments.for-post("post-lena-glaze") = fixture.comments.lena-glaze
events [ comments-requested(post: "post-lena-glaze") ]
note "the sheet mounts because the machine mounted it"
}

example load-pending {
from first-page
events [ feed-near-end() ]
note "footer spinner; the guard suppresses duplicates"
}

example load-failed {
from load-pending
events [ outcome load-next-page.err(reason: "unreachable") ]
}

example appended {
from first-page
events [
feed-near-end()
projection feed.feed-page = fixture.feed.pages-1-2
outcome load-next-page.ok()
]
}

example exhausted {
projection feed.viewer = fixture.users.mira
projection feed.feed-page = fixture.feed.final
note "cursor none, has-more false — end cap; pinned state"
}

example empty {
projection feed.viewer = fixture.users.mira
projection feed.feed-page = fixture.feed.empty
}

example failed {
projection feed.viewer = fixture.users.mira
projection feed.feed-page = failed("unreachable")
note "provider reported projection-failed"
}
Loading
Loading