From d1d1eb8ff366f9638eb97d7c1d19eddbfb8dccb6 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Thu, 2 Jul 2026 21:43:51 -0500 Subject: [PATCH 1/3] =?UTF-8?q?feat(examples):=20counter-cli=20=E2=80=94?= =?UTF-8?q?=20the=20"define=20once,=20project=20many"=20proof=20(O2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes docs/ARCHITECTURE.md non-vapor. One Counter ViewState, projected three ways in a single process with zero transport and zero substrate: - two renderers, deliberately maximally different — LineRenderer (Output=String) and GaugeRenderer (Output=Vec). Different associated Output types is the outlier-validation: if the Renderer trait fits both extremes without forcing, it fits a DOM tree / ratatui frame in the middle. - one Observer (an AI persona) that perceives the SAME Counter a human sees and, on threshold, acts through the identical command vocabulary by emitting a CommandEnvelope tagged CommandSource::Observer — the fourth projection of the identical state, not a bespoke integration. Standalone workspace member consuming positron-core as an external crate (the "consumable by anyone" proof). Two tests with `// what this catches:`: renderer purity (same &state, no mutation, byte-identical re-render) and perceive→act identity carry (observer_id() == CommandSource provenance). README status refreshed to v0.1.x + roadmap pointer; positron-ts → positron-lit for single-name consistency with the O1 nit fix. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --- Cargo.toml | 1 + README.md | 21 ++- examples/counter-cli/Cargo.toml | 19 +++ examples/counter-cli/src/main.rs | 259 +++++++++++++++++++++++++++++++ 4 files changed, 294 insertions(+), 6 deletions(-) create mode 100644 examples/counter-cli/Cargo.toml create mode 100644 examples/counter-cli/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 1aa4fec..a3485f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "positron-core", + "examples/counter-cli", ] [workspace.package] diff --git a/README.md b/README.md index 9695ae3..08c9093 100644 --- a/README.md +++ b/README.md @@ -73,12 +73,21 @@ Positron passes all four. That's why it's a separate repo. ## Status -**v0.0.0 — contract design.** The first commit defines the `ViewState` trait + `Renderer` trait + host bridge primitives in `positron-core`. No reference renderer yet — the contract gets its own moment. - -Next: -- `positron-ratatui` — terminal renderer reference impl -- `positron-ts` — TypeScript bindings + Lit DOM reference renderer -- `examples/counter-cli` — trivial demo proving the contract is renderer-agnostic +**v0.1.x — contract design + first proof.** `positron-core` defines the four +primitives (`ViewState` / `Renderer` / `Host` / `Observer`) plus the wire and +session protocols; `examples/counter-cli` proves the "define once, project many" +thesis end-to-end in one process (one `Counter` `ViewState`, two +different-`Output` renderers, one `Observer` that perceives the same state and +acts through a `CommandEnvelope`). No transport, no substrate — the contract +standing on its own. + +Run the proof: `cargo run -p counter-cli`. + +Next (see `docs/ARCHITECTURE.md` § roadmap O3–O6): +- `positron-ratatui` — terminal renderer reference impl (O3) +- `positron-lit` — Lit DOM reference renderer + regenerate `@positron/core` (O4) +- `ContinuumHost` (in continuum) — session ↔ Commands/Events, first real `ViewState` (O5) +- persona `Observer` → RAG/tool bridge (O6) - 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, and diff --git a/examples/counter-cli/Cargo.toml b/examples/counter-cli/Cargo.toml new file mode 100644 index 0000000..4f32712 --- /dev/null +++ b/examples/counter-cli/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "counter-cli" +description = "The positron 'define once, project many' proof: one ViewState, two renderers, one observer — in-process, zero transport." +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +rust-version.workspace = true +publish = false + +[[bin]] +name = "counter-cli" +path = "src/main.rs" + +[dependencies] +positron-core = { path = "../../positron-core" } +uuid = { workspace = true } +serde_json = { workspace = true } diff --git a/examples/counter-cli/src/main.rs b/examples/counter-cli/src/main.rs new file mode 100644 index 0000000..d799f6e --- /dev/null +++ b/examples/counter-cli/src/main.rs @@ -0,0 +1,259 @@ +//! # counter-cli — the positron "define once, project many" proof +//! +//! This example makes `docs/ARCHITECTURE.md` non-vapor. It demonstrates the +//! whole positron thesis in a single process with **zero transport and zero +//! substrate**: +//! +//! - **ONE app definition** — the [`Counter`] `ViewState` (what it shows) plus +//! the one command name it may emit (`counter/reset`). +//! - **Two renderers** — [`LineRenderer`] and [`GaugeRenderer`], deliberately +//! *maximally different* (single-line text vs multi-row block art, and +//! crucially **different `Renderer::Output` types**: `String` vs +//! `Vec`). This is the outlier-validation discipline: if the +//! `Renderer` trait fits both extremes without forcing, it fits the middle +//! (a DOM tree, a ratatui frame) too. +//! - **One observer** — [`ThresholdObserver`], an AI persona that perceives the +//! *same* `Counter` a human sees and, on crossing a threshold, **acts through +//! the same command vocabulary** by emitting a [`CommandEnvelope`] tagged +//! `CommandSource::Observer`. No separate "AI view," no bespoke integration — +//! the fourth projection of the identical state. +//! +//! Nothing here touches `session.rs`, `wire.rs` transport, or Continuum. Those +//! arrive at O5 (`ContinuumHost`). This unit proves the contract holds in the +//! small before any wire is involved. +//! +//! Run it: `cargo run -p counter-cli`. + +use std::sync::{Arc, Mutex}; + +use positron_core::wire::{CommandEnvelope, CommandSource}; +use positron_core::{Observer, Renderer, ViewState}; +use uuid::Uuid; + +/// The ONE app definition: a counter's value and a revision marker. +/// +/// Every surface below is a pure projection of this. It carries **semantic +/// content only** — no colors, no widths, no layout — exactly as +/// `docs/ARCHITECTURE.md` pins (open Q#4: layout is a renderer concern). A +/// terminal, a block-art gauge, and an AI observer all consume this same value. +#[derive(Debug, Clone)] +struct Counter { + value: i64, + revision: u64, +} + +impl ViewState for Counter { + fn kind(&self) -> &'static str { + "counter" + } + + fn revision(&self) -> Option { + Some(self.revision) + } +} + +/// Renderer A — a single line of human-readable text. `Output = String`. +struct LineRenderer; + +impl Renderer for LineRenderer { + type Output = String; + + fn render(&self, state: &Counter) -> String { + format!("counter = {} (rev {})", state.value, state.revision) + } +} + +/// Renderer B — the maximally-different outlier. Block-art gauge across a fixed +/// width, emitted as **multiple rows** (`Output = Vec`). Same `Counter`, +/// a wholly different surface shape and a different associated `Output` type — +/// which is the point: the trait must not privilege one surface's tree shape. +struct GaugeRenderer { + width: i64, +} + +impl Renderer for GaugeRenderer { + type Output = Vec; + + fn render(&self, state: &Counter) -> Vec { + let filled = state.value.clamp(0, self.width); + let empty = self.width - filled; + let bar = format!( + "[{}{}]", + "█".repeat(filled as usize), + "░".repeat(empty.max(0) as usize), + ); + let caption = format!("{}/{}", state.value.max(0).min(self.width), self.width); + vec![bar, caption] + } +} + +/// The one command name this app's surfaces may emit. Kept as a `const` so the +/// "command vocabulary" is a single source of truth (in a real substrate this +/// lives in Continuum, never in positron). +const RESET_COMMAND: &str = "counter/reset"; + +/// An AI persona projecting the *same* `Counter`. It does not render — it +/// perceives, at a Session-tier cognition budget — and when the value crosses +/// its threshold it acts through the identical command vocabulary a human's +/// host would use, tagging provenance as `Observer` so perception and action +/// share one identity. +struct ThresholdObserver { + id: String, + threshold: i64, + /// Where acted-upon commands land. In a real substrate this is + /// `Commands.execute`; here it is a shared sink so the example stays + /// transport-free while still proving the perceive→act loop. `Mutex>` + /// (not `mpsc::Sender`, which is `!Sync`) satisfies the `Observer: Sync` + /// bound. + emitted: Arc>>, +} + +impl Observer for ThresholdObserver { + fn observer_id(&self) -> &str { + &self.id + } + + fn budget_hz(&self) -> u32 { + // Session-tier: an AI observer's cognition can't sustain Ephemeral. + 4 + } + + fn on_change(&self, state: &Counter) { + println!( + " observer[{}] perceived rev {}: value={}", + self.id, state.revision, state.value + ); + if state.value >= self.threshold { + let cmd = CommandEnvelope { + kind: "counter".to_string(), + command: RESET_COMMAND.to_string(), + params: serde_json::json!({ "reason": "threshold", "at": state.value }), + correlation_id: Uuid::new_v4(), + source: CommandSource::Observer { + observer_id: self.id.clone(), + }, + }; + self.emitted + .lock() + .expect("emit sink mutex poisoned") + .push(cmd); + } + } +} + +fn main() { + // ONE definition, projected three ways: two renderers + one observer. + let line = LineRenderer; + let gauge = GaugeRenderer { width: 10 }; + let emitted = Arc::new(Mutex::new(Vec::new())); + let observer = ThresholdObserver { + id: "persona-asha".to_string(), + threshold: 8, + emitted: Arc::clone(&emitted), + }; + + println!("positron define-once proof — one Counter ViewState, two renderers, one observer\n"); + + // The "substrate": owns state, produces ViewState updates. Zero transport. + let mut value = 0i64; + for step in 0..6 { + value += 2; + let state = Counter { + value, + revision: step + 1, + }; + + println!("── rev {} ──────────────", state.revision().unwrap()); + // Human surfaces — same state, different projections, neither mutates it. + println!(" line : {}", line.render(&state)); + for row in gauge.render(&state) { + println!(" gauge: {row}"); + } + // AI surface — same state, perceived not rendered. + observer.on_change(&state); + println!(); + } + + // Drain what the observer acted on — perceive→act through the SAME frame. + let commands = emitted.lock().expect("emit sink mutex poisoned"); + println!( + "observer emitted {} command(s) through the shared vocabulary:", + commands.len() + ); + for c in commands.iter() { + println!( + " {} {} (source={:?}, corr={})", + c.kind, c.command, c.source, c.correlation_id + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // what this catches: the two renderers are pure projections of the SAME + // Counter — different Output types, neither mutating state. If someone + // reintroduces widget-local mutable history (the anti-pattern positron + // exists to kill), rendering the same state twice would diverge or the + // borrow would fail to compile. + #[test] + fn both_renderers_project_the_same_state_without_mutation() { + let state = Counter { + value: 4, + revision: 2, + }; + let line = LineRenderer; + let gauge = GaugeRenderer { width: 10 }; + + // Same &state feeds both renderers (shared immutable borrow proves + // neither takes &mut). + let line_out: String = line.render(&state); + let gauge_out: Vec = gauge.render(&state); + + assert_eq!(line_out, "counter = 4 (rev 2)"); + assert_eq!(gauge_out, vec!["[████░░░░░░]".to_string(), "4/10".to_string()]); + + // Rendering again yields byte-identical output — no hidden state drift. + assert_eq!(line.render(&state), line_out); + assert_eq!(gauge.render(&state), gauge_out); + } + + // what this catches: the observer perceives the same ViewState and, on + // threshold, acts through the identical command vocabulary — with its + // perception identity carried onto the action provenance + // (CommandSource::Observer { observer_id } == observer_id()). This is the + // load-bearing positron claim: the AI is the fourth projection, not a + // bespoke path. + #[test] + fn observer_perceives_then_acts_with_carried_identity() { + let emitted = Arc::new(Mutex::new(Vec::new())); + let observer = ThresholdObserver { + id: "persona-asha".to_string(), + threshold: 8, + emitted: Arc::clone(&emitted), + }; + + // Below threshold: perceives, does not act. + observer.on_change(&Counter { + value: 6, + revision: 1, + }); + assert!(emitted.lock().unwrap().is_empty()); + + // At/over threshold: acts once, through the shared command name. + observer.on_change(&Counter { + value: 8, + revision: 2, + }); + let cmds = emitted.lock().unwrap(); + assert_eq!(cmds.len(), 1); + assert_eq!(cmds[0].command, RESET_COMMAND); + match &cmds[0].source { + CommandSource::Observer { observer_id } => { + assert_eq!(observer_id, observer.observer_id()); + } + other => panic!("expected Observer provenance, got {other:?}"), + } + } +} From 6862cf013cc115ebc8abefa78c5aacc93e43a49a Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Thu, 2 Jul 2026 21:46:54 -0500 Subject: [PATCH 2/3] =?UTF-8?q?docs(examples):=20address=20review=20nits?= =?UTF-8?q?=20=E2=80=94=20level-triggered=20prose,=20drop=20latent=20unwra?= =?UTF-8?q?p,=20clamp=20idiom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review (APPROVE WITH NITS) touch-ups: - ThresholdObserver doc now says "while at/over its threshold" and states it's level-triggered (emits on every at/over-threshold perception, not only the crossing) — matches the code, which fires 3× for 8/10/12. - print state.revision directly instead of state.revision().unwrap() (the field is owned; no reason to round-trip through the Option-returning trait method). - GaugeRenderer caption uses .clamp(0, width) to match the idiom one line up. No behavior change. cargo test -p counter-cli → 2 pass; clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --- examples/counter-cli/src/main.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/counter-cli/src/main.rs b/examples/counter-cli/src/main.rs index d799f6e..1e82859 100644 --- a/examples/counter-cli/src/main.rs +++ b/examples/counter-cli/src/main.rs @@ -82,7 +82,7 @@ impl Renderer for GaugeRenderer { "█".repeat(filled as usize), "░".repeat(empty.max(0) as usize), ); - let caption = format!("{}/{}", state.value.max(0).min(self.width), self.width); + let caption = format!("{}/{}", state.value.clamp(0, self.width), self.width); vec![bar, caption] } } @@ -93,10 +93,11 @@ impl Renderer for GaugeRenderer { const RESET_COMMAND: &str = "counter/reset"; /// An AI persona projecting the *same* `Counter`. It does not render — it -/// perceives, at a Session-tier cognition budget — and when the value crosses -/// its threshold it acts through the identical command vocabulary a human's -/// host would use, tagging provenance as `Observer` so perception and action -/// share one identity. +/// perceives, at a Session-tier cognition budget — and while the value sits +/// at/over its threshold it acts through the identical command vocabulary a +/// human's host would use, tagging provenance as `Observer` so perception and +/// action share one identity. (Level-triggered, not edge-triggered: it emits on +/// every perceived state that is at/over threshold, not only on the crossing.) struct ThresholdObserver { id: String, threshold: i64, @@ -163,7 +164,7 @@ fn main() { revision: step + 1, }; - println!("── rev {} ──────────────", state.revision().unwrap()); + println!("── rev {} ──────────────", state.revision); // Human surfaces — same state, different projections, neither mutates it. println!(" line : {}", line.render(&state)); for row in gauge.render(&state) { From 74621bdff2a80499697c91d24f5bca3e07d6a082 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Thu, 2 Jul 2026 21:47:49 -0500 Subject: [PATCH 3/3] style: cargo fmt counter-cli (CI fmt gate) Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --- examples/counter-cli/src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/counter-cli/src/main.rs b/examples/counter-cli/src/main.rs index 1e82859..a76e9bb 100644 --- a/examples/counter-cli/src/main.rs +++ b/examples/counter-cli/src/main.rs @@ -213,7 +213,10 @@ mod tests { let gauge_out: Vec = gauge.render(&state); assert_eq!(line_out, "counter = 4 (rev 2)"); - assert_eq!(gauge_out, vec!["[████░░░░░░]".to_string(), "4/10".to_string()]); + assert_eq!( + gauge_out, + vec!["[████░░░░░░]".to_string(), "4/10".to_string()] + ); // Rendering again yields byte-identical output — no hidden state drift. assert_eq!(line.render(&state), line_out);