feat: pluresdb-px — .px language runtime as foundation crate#974
Open
kayodebristol wants to merge 3 commits into
Open
feat: pluresdb-px — .px language runtime as foundation crate#974kayodebristol wants to merge 3 commits into
kayodebristol wants to merge 3 commits into
Conversation
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.
Contributor
There was a problem hiding this comment.
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-pxcrate implementing.pxparsing (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
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extracts the
.pxdeclarative language runtime frompares-radix-praxisintopluresdb-px, making it a foundation-layer crate available to any application built on PluresDB.What's included (16,081 LOC + 484 tests)
.pxsource files.pxAST → PluresDB-compatible JSON procedure recordsActionHandler.pxmistakes.pxfiles.pxprocedure verificationWhy
The
.pxruntime is the bridge between writing declarative logic and executing it reactively on PluresDB. It was trapped insidepares-radixdespite having zero dependencies on pares-radix internals. This extraction enables the ideal application development path:Verification
cargo build -p pluresdb-px— cleancargo clippy -p pluresdb-px -- -D warnings— 0 warningscargo test -p pluresdb-px— 484 passed, 0 failedpluresdb-sea)Next steps
pares-radixto depend onpluresdb-pxinstead of maintaining its own copypluresdb-pxexecutor intopluresdb-proceduresAgensRuntimefor reactive.px→ event dispatchActionHandlerimplementations (shell, HTTP, filesystem) aspluresdb-px-actions