Skip to content

feat: pluresdb-px — .px language runtime as foundation crate#974

Open
kayodebristol wants to merge 3 commits into
mainfrom
feature/pluresdb-px
Open

feat: pluresdb-px — .px language runtime as foundation crate#974
kayodebristol wants to merge 3 commits into
mainfrom
feature/pluresdb-px

Conversation

@kayodebristol
Copy link
Copy Markdown
Contributor

Summary

Extracts the .px declarative language runtime from pares-radix-praxis into pluresdb-px, making it a foundation-layer crate available to any application built on PluresDB.

What's included (16,081 LOC + 484 tests)

  • Parser — pest PEG grammar for .px source files
  • Compiler.px AST → PluresDB-compatible JSON procedure records
  • Executor — walks compiled procedures through pluggable ActionHandler
  • Async Executor — parallel branches, retry with backoff, timeouts
  • Linter — static analysis for common .px mistakes
  • Resolver — import resolution across .px files
  • Watcher — filesystem watcher for hot-reload on changes
  • Compose — dynamic procedure composition and pipeline building
  • Scenario Runner — test harness for .px procedure verification
  • Constraint DB — in-memory constraint store and evaluation engine

Why

The .px runtime is the bridge between writing declarative logic and executing it reactively on PluresDB. It was trapped inside pares-radix despite having zero dependencies on pares-radix internals. This extraction enables the ideal application development path:

1. Write .px files (your application logic)
2. pluresdb-px compiles them → PluresDB nodes
3. PluresDB provides persistence + sync + reactive triggers for free
4. You implement ActionHandlers for your specific IO boundary
5. Done.

Verification

  • cargo build -p pluresdb-px — clean
  • cargo clippy -p pluresdb-px -- -D warnings — 0 warnings
  • cargo test -p pluresdb-px — 484 passed, 0 failed
  • ✅ Full workspace builds (excluding pre-broken pluresdb-sea)

Next steps

  1. Update pares-radix to depend on pluresdb-px instead of maintaining its own copy
  2. Wire pluresdb-px executor into pluresdb-procedures AgensRuntime for reactive .px → event dispatch
  3. Publish standard ActionHandler implementations (shell, HTTP, filesystem) as pluresdb-px-actions

The .px declarative language runtime (parser, compiler, executor, async
executor, linter, resolver, watcher, compose, scenario runner) has been
extracted from pares-radix-praxis into pluresdb-px.

This makes the .px → PluresDB pipeline available to ANY application:
1. Write .px files (application logic)
2. pluresdb-px compiles them → PluresDB-compatible procedure records
3. PluresDB provides persistence + sync + reactive triggers
4. Application provides ActionHandler implementations for its IO

The code has zero dependencies on pares-radix — it never did. The
coupling was purely organizational (living in the wrong repo).

484 tests passing, 0 clippy warnings, clean build.

pares-radix will be updated to depend on pluresdb-px instead of
maintaining its own copy.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extracts the .px declarative language runtime into a new foundation crate (pluresdb-px) so multiple PluresDB-based apps can parse/compile/execute .px procedures without depending on pares-radix internals.

Changes:

  • Adds the pluresdb-px crate implementing .px parsing (pest), AST building, compilation to JSON records, and sync/async execution utilities.
  • Introduces runtime utilities: import resolver, hot-reload watcher, procedure composition, and a scenario runner test harness.
  • Adds an in-memory “praxis store” layer (db/*) with schema, procedures, guidance storage, and seeded constraints/ADRs/evidence.

Reviewed changes

Copilot reviewed 19 out of 21 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
crates/pluresdb-px/src/px/watcher.rs New filesystem watcher emitting hot-reload events for .px files.
crates/pluresdb-px/src/px/scenario_runner.rs New scenario runner for executing compiled scenarios and checking expectations.
crates/pluresdb-px/src/px/resolver.rs New import resolver for recursively loading/merging .px imports.
crates/pluresdb-px/src/px/mod.rs New .px module root exporting parser/compiler/executors/utilities.
crates/pluresdb-px/src/px/grammar.pest Pest grammar defining .px language syntax.
crates/pluresdb-px/src/px/compose.rs Procedure composition utilities + composable async handler.
crates/pluresdb-px/src/px/compiler.rs Compiler from AST to PluresDB-ready JSON records + stats/lint integration.
crates/pluresdb-px/src/px/builder.rs AST builder from pest parse tree into typed structures.
crates/pluresdb-px/src/lib.rs Crate-level docs and module exports for px and db.
crates/pluresdb-px/src/db/store.rs In-memory store for constraints/ADRs/evidence with traversal helpers.
crates/pluresdb-px/src/db/seed.rs Seed data and default store builder for built-in constraints/ADRs/evidence.
crates/pluresdb-px/src/db/schema.rs Schema types (Constraint/ADR/Evidence/AgentContext) and condition evaluation.
crates/pluresdb-px/src/db/procedures.rs Procedures for evaluation, action blocking, gap querying, corrections.
crates/pluresdb-px/src/db/mod.rs DB module exports/re-exports for ergonomic imports.
crates/pluresdb-px/src/db/guidance.rs In-memory guidance store + schema for guidance/spans/events.
crates/pluresdb-px/Cargo.toml New crate manifest with optional watcher feature/deps.
Cargo.toml Adds crates/pluresdb-px to the workspace members.
Cargo.lock Locks new dependency graph entries (notably notify + platform backends).

pub mod executor;
pub mod lint;
pub mod resolver;
pub mod scenario_runner;
Comment on lines +211 to +225
// Handle relative file paths
let mut path = base_path.join(import_path);
if path.extension().is_none() {
path.set_extension("px");
}

// Reject absolute paths that aren't under base_path
if import_path.starts_with('/') {
return Err(ResolveError::InvalidPath {
import_path: import_path.to_string(),
message: "absolute import paths are not allowed".to_string(),
});
}

Ok(path)
let (tx, rx) = mpsc::channel(256);
let config = self.config.clone();
let key_index: KeyIndex = Arc::new(Mutex::new(HashMap::new()));

Comment on lines +321 to +336
// 2. Execute the run procedure (if specified)
if let Some(run_info) = scenario_data.get("run").filter(|v| !v.is_null()) {
let proc_name = if let Some(name_str) = run_info.as_str() {
name_str.to_string()
} else if let Some(n) = run_info.get("procedure").and_then(|v| v.as_str()) {
n.to_string()
} else {
return ScenarioResult {
name,
given,
passed: false,
expectations: vec![],
error: Some("invalid run clause".to_string()),
duration_ms: start.elapsed().as_millis() as u64,
};
};
Comment on lines +475 to +487
"when" => {
let condition = step
.get("condition")
.and_then(|v| v.as_str())
.unwrap_or("true");
if handler.evaluate_condition(condition, &HashMap::new()) {
if let Some(steps) = step.get("steps").and_then(|v| v.as_array()) {
for s in steps {
execute_step(s, handler)?;
}
}
}
Ok(Value::Null)
Comment on lines +93 to +98
#![allow(missing_docs)] // TODO: re-enable once API stabilizes

/// The `.px` language runtime: parser, compiler, executors, linter, resolver.
pub mod px;

/// Constraint store and evaluation engine (in-memory, zero external deps).
//! // 1. Parse .px source
//! let doc = parse(r#"
//! procedure greet:
//! trigger: message
Chronos (graph-native application state chronicle) extracted from
pares-radix into pluresdb-chronos. Provides zero-effort observability
through causal state diffs stored in PluresDB.

Features:
- Causal version chains (parent_id linking)
- Actor attribution
- Level filtering (Debug/Info/Warn/Error)
- JSONL file sink for cross-machine analysis
- Timeline queries: by key, actor, time range, severity
- Replay between two points

Depends only on pluresdb-core (CrdtStore), serde, sha2, uuid, chrono.
13 tests passing, 0 clippy warnings.

The PluresDB crate collection now provides:
- pluresdb-core: CRDT store + persistence
- pluresdb-procedures: query DSL + reactive AgensRuntime
- pluresdb-px: .px language runtime (parser/compiler/executor)
- pluresdb-chronos: state timeline + observability
- pluresdb-sync: P2P Hyperswarm replication

One dependency. Complete application runtime.
Adds browser-side access to the full PluresDB application runtime:

- pxCompile(source) → parse + compile .px source to records
- pxParse(source) → AST for introspection
- pxLint(source) → static analysis diagnostics
- pxExecute(record, handler) → run a procedure with JS callback handler
- WasmChronosTimeline → record/history/timeline/recent/setLevel

Also:
- Made pluresdb-px features modular: async/watcher are optional
  (sync executor, parser, compiler, linter work without tokio)
- Made pluresdb-chronos native feature optional for WASM builds
- Added Serialize derives to CompiledRecord and LintDiagnostic
- WASM target (wasm32-unknown-unknown) builds clean
Copilot AI review requested due to automatic review settings May 20, 2026 14:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 27 changed files in this pull request and generated 11 comments.

Comment on lines +10 to +16
compiler::{compile, CompiledRecord},
executor::{self, ActionHandler, ExecutionError, ExecutionResult},
};

use std::cell::RefCell;
use std::collections::HashMap;

Comment on lines +63 to +71
/// SAFETY: wasm32 is single-threaded, so Send+Sync is vacuously satisfied.
struct JsActionHandler {
callback: Function,
}

// SAFETY: WASM is single-threaded; Function cannot be shared across threads
// because there are no threads.
unsafe impl Send for JsActionHandler {}
unsafe impl Sync for JsActionHandler {}
Comment on lines +93 to +94
#![allow(missing_docs)] // TODO: re-enable once API stabilizes

//!
//! // 3. Execute with your action handler
//! let handler = MyHandler::new();
//! for record in records.iter().filter(|r| r.record_type == "procedure") {
Comment on lines +186 to +225
fn resolve_import_path(import_path: &str, base_path: &Path) -> Result<PathBuf, ResolveError> {
if import_path.is_empty() {
return Err(ResolveError::InvalidPath {
import_path: import_path.to_string(),
message: "empty import path".to_string(),
});
}

// Handle Rust-style paths (module::sub)
if import_path.contains("::") {
let parts: Vec<&str> = import_path.split("::").collect();
if parts.iter().any(|p| p.is_empty()) {
return Err(ResolveError::InvalidPath {
import_path: import_path.to_string(),
message: "empty segment in import path".to_string(),
});
}
let mut path = base_path.to_path_buf();
for part in &parts {
path.push(part);
}
path.set_extension("px");
return Ok(path);
}

// Handle relative file paths
let mut path = base_path.join(import_path);
if path.extension().is_none() {
path.set_extension("px");
}

// Reject absolute paths that aren't under base_path
if import_path.starts_with('/') {
return Err(ResolveError::InvalidPath {
import_path: import_path.to_string(),
message: "absolute import paths are not allowed".to_string(),
});
}

Ok(path)
Comment on lines +128 to +154
tokio::spawn(async move {
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};

let (notify_tx, mut notify_rx) = tokio::sync::mpsc::channel::<Event>(256);

let mut watcher = match RecommendedWatcher::new(
move |res: Result<Event, notify::Error>| {
if let Ok(event) = res {
let _ = notify_tx.blocking_send(event);
}
},
notify::Config::default(),
) {
Ok(w) => w,
Err(e) => {
warn!("failed to create .px watcher: {}", e);
return;
}
};

if let Err(e) = watcher.watch(&config.watch_path, RecursiveMode::Recursive) {
warn!(path = %config.watch_path.display(), "failed to watch for .px: {}", e);
return;
}

info!(path = %config.watch_path.display(), "watching for .px file changes");

Comment on lines +321 to +352
// 2. Execute the run procedure (if specified)
if let Some(run_info) = scenario_data.get("run").filter(|v| !v.is_null()) {
let proc_name = if let Some(name_str) = run_info.as_str() {
name_str.to_string()
} else if let Some(n) = run_info.get("procedure").and_then(|v| v.as_str()) {
n.to_string()
} else {
return ScenarioResult {
name,
given,
passed: false,
expectations: vec![],
error: Some("invalid run clause".to_string()),
duration_ms: start.elapsed().as_millis() as u64,
};
};

if let Some(proc_data) = procedures.get(&proc_name) {
if let Some(steps) = proc_data.get("steps").and_then(|v| v.as_array()) {
for step in steps {
if let Err(e) = execute_step(step, &handler) {
return ScenarioResult {
name,
given,
passed: false,
expectations: vec![],
error: Some(format!("procedure '{proc_name}' failed: {e}")),
duration_ms: start.elapsed().as_millis() as u64,
};
}
}
}
Comment on lines +104 to +108
pub struct ComposableHandler<H: AsyncActionHandler> {
registry: ProcedureRegistry,
inner: H,
depth: AtomicUsize,
}
Comment on lines +397 to +403
let mut entries: Vec<ChronosEntry> = self
.store
.list()
.into_iter()
.filter_map(|r| {
let e: ChronosEntry = serde_json::from_value(r.data).ok()?;
if let Some(ts) = since {
Comment on lines +397 to +405
let mut entries: Vec<ChronosEntry> = self
.store
.list()
.into_iter()
.filter_map(|r| {
let e: ChronosEntry = serde_json::from_value(r.data).ok()?;
if let Some(ts) = since {
if e.timestamp < ts {
return None;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants