Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "2"
members = [
"positron-core",
"examples/counter-cli",
]

[workspace.package]
Expand Down
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions examples/counter-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
263 changes: 263 additions & 0 deletions examples/counter-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
//! # 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<String>`). 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<u64> {
Some(self.revision)
}
}

/// Renderer A — a single line of human-readable text. `Output = String`.
struct LineRenderer;

impl Renderer<Counter> 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<String>`). 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<Counter> for GaugeRenderer {
type Output = Vec<String>;

fn render(&self, state: &Counter) -> Vec<String> {
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.clamp(0, 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 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,
/// 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<Vec<_>>`
/// (not `mpsc::Sender`, which is `!Sync`) satisfies the `Observer: Sync`
/// bound.
emitted: Arc<Mutex<Vec<CommandEnvelope>>>,
}

impl Observer<Counter> 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);
// 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<String> = 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:?}"),
}
}
}
Loading