From 43bd5b7366edc9506bed4ff315b0006ca2b24b42 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Thu, 2 Jul 2026 21:37:54 -0500 Subject: [PATCH 1/2] =?UTF-8?q?docs(architecture):=20define-once=20separat?= =?UTF-8?q?ion=20contract=20=E2=80=94=20positron=20lowers=20onto=20Command?= =?UTF-8?q?s/Events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four primitives are the "define once, project many" mechanism, but the separation of concerns was never written down — and the load-bearing decision (positron's session protocol vs the substrate's own command/event wire) was unresolved, risking two parallel dispatchers. docs/ARCHITECTURE.md pins it: - one (ViewState, command-vocabulary) definition → four surfaces collapse to two roles: Renderer (web/terminal/mobile) + Observer (AI persona → RAG/tool). - positron owns the view/render/perceive contract + wire semantics + session; it does NOT own a second dispatch bus. CommandEnvelope LOWERS to Commands.execute; StateEnvelope is produced by a projector over Events; the ContinuumHost is the single lowering seam. - constrains DESIGN.md open Qs #3/#4 to renderer-concern / semantic-only ViewState. - O1..O6 roadmap, one PR per unit; O2 (counter-cli) is next and makes this non-vapor. README + DESIGN cross-link the new doc. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --- DESIGN.md | 7 ++ README.md | 5 +- docs/ARCHITECTURE.md | 149 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 docs/ARCHITECTURE.md diff --git a/DESIGN.md b/DESIGN.md index 9eab2a2..c6b8f74 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -137,6 +137,13 @@ This is the throttling primitive that lets one `ViewState` source serve a 60fps These are TBD before v1.0; the v0 contract above is the foundation regardless of which way they resolve. +> **Note:** `docs/ARCHITECTURE.md` (the "define once, project many" separation +> contract) now *constrains* two of these: #3 (theming) and #4 (layout) resolve to +> **renderer concern / semantic-content-only `ViewState`** — not a lean but a +> requirement, because one `ViewState` serving a terminal, a DOM, and an AI +> observer cannot carry surface geometry or theme. Diffing (#1) and async arrival +> (#2) remain open. + --- ## Versioning diff --git a/README.md b/README.md index 4073e55..9695ae3 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,10 @@ Next: - `examples/counter-cli` — trivial demo proving the contract is renderer-agnostic - Theme pack (Loki / Matrix / Fallout / Tron) ported from the cyberpunk-cli experiment -See `DESIGN.md` for the contract design and the rationale behind each trait. +See `DESIGN.md` for the contract design and the rationale behind each trait, and +`docs/ARCHITECTURE.md` for the **separation of concerns** — how one app definition +projects to web, terminal, mobile, and an AI persona, and where positron ends and +the substrate's `Commands`/`Events` begin. --- diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..8050b0d --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,149 @@ +# Positron Architecture — Define Once, Project Many + +> `DESIGN.md` defines the four primitives. This doc defines the **separation of +> concerns** between them and the substrate they sit on — the load-bearing +> contract that lets one app definition serve web, terminal, mobile, and an AI +> persona without any surface reinventing state, dispatch, or perception. + +## The thesis + +**An app is defined once as a `(ViewState, command-vocabulary)` pair. Every +surface is a pure projection of it.** Adding a surface is writing one function, +not rewriting the widget. + +``` + ┌───────────────────────────────┐ + │ ONE definition │ + │ • ViewState (what it shows) │ + │ • commands (what it does) │ + │ • StateLayer (how fast) │ + └───────────────┬───────────────┘ + │ projected by + ┌──────────────┬────────────┼────────────┬──────────────────┐ + ▼ ▼ ▼ ▼ ▼ + Renderer Renderer Renderer Renderer Observer + → DOM → cells → native (future) → RAG + (web/Lit) (terminal) (mobile) (AI persona) + acts via CommandEnvelope +``` + +The four "environments" collapse to **two roles**: + +| Role | Consumes | Produces | Surfaces | +|---|---|---|---| +| `Renderer` | the `ViewState` | a surface tree | web (Lit DOM), terminal (ratatui), mobile (native), AR/VR (Bevy) | +| `Observer` | the **same** `ViewState` | perception → RAG, then `CommandEnvelope`s | AI persona | + +The AI persona is **not a special case**. It is the fourth projection of the +identical `ViewState` — it perceives what a human sees, and it acts through the +same command vocabulary a human's host emits. That is the positron principle, +made structural. + +## Separation of concerns — the layer boundary (the load-bearing part) + +Positron **owns**: + +- the view/render/perceive **contract** — `ViewState` / `Renderer` / `Host` / `Observer` +- the state/command **wire semantics** — `StateEnvelope` / `CommandEnvelope` / `ObserverSpec` +- the **session protocol** — snapshot-then-live resync (`ClientMessage` / `ServerMessage`) + +Positron does **NOT** own — and must never grow: + +- state management (substrates use whatever they want to *produce* `ViewState`) +- a command **vocabulary** (`chat/send` etc. are the consumer's) +- a **transport** (WebSocket / UDS / airc are the substrate's) +- **a second dispatch bus or event bus** ← the anti-pattern this doc exists to forbid + +The substrate (Continuum is the reference consumer) owns state + the command +vocabulary, exposed through its **two universal primitives**: `Commands.execute` +(request/response) and `Events` (publish/subscribe). + +### positron session **lowers onto** Commands/Events — it does not compete + +The one rule that keeps this from becoming two parallel systems: + +| positron frame | lowers to (substrate) | +|---|---| +| `ClientMessage::Command(CommandEnvelope)` | `Commands.execute(command, params)` — **the one dispatch owner**, not a second | +| `ClientMessage::Subscribe { kinds, layers }` | subscribe to the `Events` that feed those `ViewState` projections; substrate emits `ServerMessage::State` snapshot-then-live | +| `ClientMessage::Observe { spec }` | the same subscription, scoped by the observer's cognition budget | +| `ServerMessage::State(StateEnvelope)` | emitted by a **projector**: a substrate-side task that subscribes to `Events` and builds the `ViewState` | +| `ServerMessage::CommandFailed` | the loud failure path of `Commands.execute` (success needs no ack — the state change *is* the ack) | + +The seam that performs this lowering is the **`ContinuumHost`** (already named in +`session.rs` tests). It is the single adapter where positron frames meet +`Commands`/`Events`. Positron contributes the frame; the substrate contributes +the dispatch and the bus. + +### Why "two wire protocols" is not a duplication + +- `continuum/ipc/ws.rs` (`WsClientMessage`/`WsServerMessage`, correlation-id + command RPC) = the **raw** Commands/Events transport for thin clients. +- `positron/session.rs` (`Subscribe`/`Command`/`Observe` → `State`) = the + **view-projection** protocol layered on top. + +Resolution: **positron session is implemented *over* the substrate primitives.** +`ContinuumHost` maps `Subscribe → Events-subscription` and `Command → +Commands.execute`. One dispatch, one bus; positron adds view-projection, +snapshot-then-live resync, and AI-observer budgeting that the raw ws protocol +does not carry. + +> **Open, deliberately not pre-decided:** whether the two wires should eventually +> *merge* — positron's session protocol becoming Continuum's canonical +> thin-client wire, retiring the bespoke `WsClientMessage`. That is a real +> candidate, but it is an **integration-time** decision (task O5 below), made with +> both protocols in front of us, not a guess made now. Flag it; don't foreclose it. + +## Where each piece lives (separation of repos) + +| Concern | Home | Why | +|---|---|---| +| The four traits + wire + session | **positron** (`positron-core`) | consumer-agnostic contract; independently versioned (the "airc test") | +| Reference renderer crates (`positron-ratatui`, `positron-ts`/lit) | **positron** | surface projections anyone in the problem domain reuses | +| Concrete `ViewState` types (`ChatViewState`, …) | **continuum** | domain vocabulary — positron never knows what a "chat message" is | +| Command vocabulary (360+ commands) | **continuum** | already the single source of truth; positron frames, does not define | +| `ContinuumHost` adapter (session ↔ Commands/Events) | **continuum** | binds the contract to *this* substrate's primitives | +| Persona `Observer` → RAG/tool bridge | **continuum** | perception into cognition is a substrate concern | +| The `apps/` (web/desktop/mobile) | **continuum** | consumers of the renderer crates | + +## The "define once" mechanics (the compression) + +To **define** a view: one `ViewState` type + the command names it may emit +(already in continuum) + the `StateLayer` it emits at. + +- To add a **surface**: write **one** `Renderer`. Nothing else. +- To make it **AI-perceivable**: write **zero** extra code — the `Observer` + consumes the same `ViewState`. + +That is the compression principle at the UI layer: one logical view, one place; +the surfaces are pure functions of it. The anti-pattern it kills is the +2000-line widget where signal stores, instance fields, and global registries +fight to be the source of truth (see `DESIGN.md` § Renderer). If you can't render +the view from `ViewState` alone, the state type is incomplete — not the renderer. + +## Constraints this doc pins on DESIGN.md's open questions + +- **Layout (open Q #4): semantic content only.** "One `ViewState`, four surfaces" + *requires* it — a terminal cannot consume DOM pixel coordinates, and a persona + perceives meaning, not geometry. `ViewState` carries semantics; each `Renderer` + owns its own layout. This is now a contract constraint, not a lean. +- **Theming (open Q #3): renderer concern.** Same reasoning — state stays + presentation-free; a theme is a property of a surface, not of the app definition. +- **`positron-ts` vs continuum `sdk/typescript`: decided at O4, not before.** Both + generate types from Rust via ts-rs; whether positron-ts *subsumes* or + *complements* the continuum SDK is resolved when the web renderer actually lands. + +## Organizational task roadmap (one PR per unit) + +| # | Unit | Proves | Depends on | +|---|---|---|---| +| **O1** | This separation contract | the boundary is pinned before any renderer is written | — | +| **O2** | `examples/counter-cli` | renderer-agnostic **and** AI-perceives-same-state, in-process, zero transport (one `Counter` `ViewState`, ≥2 `Renderer`s, 1 `Observer`) | O1 | +| **O3** | `positron-ratatui` | terminal `Renderer` reference (outlier A surface) | O2 | +| **O4** | `positron-ts` / lit | web DOM `Renderer` + regenerate `@positron/core`; reconcile with continuum `sdk/typescript` (subsume vs complement) | O2 | +| **O5** | `ContinuumHost` (in continuum) | positron session ↔ Commands/Events; first real `ViewState` (`ChatViewState`) flows to a positron renderer; resolves the two-wire merge question | O3 or O4 | +| **O6** | persona `Observer` → RAG/tool bridge (in continuum) | perception into cognition + action as `CommandEnvelope` — closes "AI persona rag/tool integration" | O5 | + +O2 is the next unit: it makes this doc non-vapor by proving "one `ViewState`, +many renderers, plus an observer perceiving the same state" in a single process, +before any transport or substrate is involved. From 82bbc07dbd22179f79f99f4caa92ffd1406feaa8 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Thu, 2 Jul 2026 21:40:05 -0500 Subject: [PATCH 2/2] =?UTF-8?q?docs(architecture):=20address=20review=20ni?= =?UTF-8?q?ts=20=E2=80=94=20positron-lit=20naming,=20ContinuumHost=20phras?= =?UTF-8?q?ing,=20theming=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review (APPROVE WITH NITS) fixes: - use the established crate name `positron-lit` (not a new `positron-ts`); the npm types package stays `@positron/core`. Single name, one place. - soften "ContinuumHost already named in session.rs tests" → "referenced by name in a session.rs test comment (not yet a wired symbol)" — it's a prose mention, not an exercised symbol. - theming (open Q#3) framed as a strong default, not a primitive-enforced constraint: a theme token is arguably semantic, unlike layout geometry. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --- docs/ARCHITECTURE.md | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8050b0d..576a419 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -70,10 +70,10 @@ The one rule that keeps this from becoming two parallel systems: | `ServerMessage::State(StateEnvelope)` | emitted by a **projector**: a substrate-side task that subscribes to `Events` and builds the `ViewState` | | `ServerMessage::CommandFailed` | the loud failure path of `Commands.execute` (success needs no ack — the state change *is* the ack) | -The seam that performs this lowering is the **`ContinuumHost`** (already named in -`session.rs` tests). It is the single adapter where positron frames meet -`Commands`/`Events`. Positron contributes the frame; the substrate contributes -the dispatch and the bus. +The seam that performs this lowering is the **`ContinuumHost`** (referenced by +name in a `session.rs` test comment — not yet a wired symbol). It is the single +adapter where positron frames meet `Commands`/`Events`. Positron contributes the +frame; the substrate contributes the dispatch and the bus. ### Why "two wire protocols" is not a duplication @@ -99,7 +99,7 @@ does not carry. | Concern | Home | Why | |---|---|---| | The four traits + wire + session | **positron** (`positron-core`) | consumer-agnostic contract; independently versioned (the "airc test") | -| Reference renderer crates (`positron-ratatui`, `positron-ts`/lit) | **positron** | surface projections anyone in the problem domain reuses | +| Reference renderer crates (`positron-ratatui`, `positron-lit`) | **positron** | surface projections anyone in the problem domain reuses | | Concrete `ViewState` types (`ChatViewState`, …) | **continuum** | domain vocabulary — positron never knows what a "chat message" is | | Command vocabulary (360+ commands) | **continuum** | already the single source of truth; positron frames, does not define | | `ContinuumHost` adapter (session ↔ Commands/Events) | **continuum** | binds the contract to *this* substrate's primitives | @@ -127,11 +127,16 @@ the view from `ViewState` alone, the state type is incomplete — not the render *requires* it — a terminal cannot consume DOM pixel coordinates, and a persona perceives meaning, not geometry. `ViewState` carries semantics; each `Renderer` owns its own layout. This is now a contract constraint, not a lean. -- **Theming (open Q #3): renderer concern.** Same reasoning — state stays - presentation-free; a theme is a property of a surface, not of the app definition. -- **`positron-ts` vs continuum `sdk/typescript`: decided at O4, not before.** Both - generate types from Rust via ts-rs; whether positron-ts *subsumes* or - *complements* the continuum SDK is resolved when the web renderer actually lands. +- **Theming (open Q #3): renderer concern (strong default, not a hard structural + constraint).** Same *direction* as layout — presentation belongs to the surface, + so `ViewState` stays theme-free and each `Renderer` knows themes. Unlike layout + geometry, a theme *token* is arguably semantic and could ride in state, so this + is the recommended default rather than a primitive-enforced necessity; DESIGN.md + leans the same way. +- **`@positron/core` (npm) + `positron-lit` vs continuum `sdk/typescript`: decided + at O4, not before.** Both generate types from Rust via ts-rs; whether the positron + web stack *subsumes* or *complements* the continuum SDK is resolved when the web + renderer actually lands. ## Organizational task roadmap (one PR per unit) @@ -140,7 +145,7 @@ the view from `ViewState` alone, the state type is incomplete — not the render | **O1** | This separation contract | the boundary is pinned before any renderer is written | — | | **O2** | `examples/counter-cli` | renderer-agnostic **and** AI-perceives-same-state, in-process, zero transport (one `Counter` `ViewState`, ≥2 `Renderer`s, 1 `Observer`) | O1 | | **O3** | `positron-ratatui` | terminal `Renderer` reference (outlier A surface) | O2 | -| **O4** | `positron-ts` / lit | web DOM `Renderer` + regenerate `@positron/core`; reconcile with continuum `sdk/typescript` (subsume vs complement) | O2 | +| **O4** | `positron-lit` | web DOM `Renderer` + regenerate `@positron/core`; reconcile with continuum `sdk/typescript` (subsume vs complement) | O2 | | **O5** | `ContinuumHost` (in continuum) | positron session ↔ Commands/Events; first real `ViewState` (`ChatViewState`) flows to a positron renderer; resolves the two-wire merge question | O3 or O4 | | **O6** | persona `Observer` → RAG/tool bridge (in continuum) | perception into cognition + action as `CommandEnvelope` — closes "AI persona rag/tool integration" | O5 |