diff --git a/crates/runvil-internal/engine/src/error/tests-chain.rs b/crates/runvil-internal/engine/src/error/tests-chain.rs index 6d25d0497..15ddad245 100644 --- a/crates/runvil-internal/engine/src/error/tests-chain.rs +++ b/crates/runvil-internal/engine/src/error/tests-chain.rs @@ -228,7 +228,7 @@ fn a_parse_failure_run_raises_names_the_prompt_and_its_file() { let code = ErrorCode::StructureInvalid; let message = "not a runvil prompt: no runvil version"; let stamped = crate::prompt::detail::with_prompt_context( - crate::prompt::detail::parse_error(code, message), + runvil_types::error::failure(code, message), "greeter", "agents/greeter.md", ); diff --git a/crates/runvil-internal/engine/src/execute/scheduler/drive.rs b/crates/runvil-internal/engine/src/execute/scheduler/drive.rs index 70a5c0414..dff48b19a 100644 --- a/crates/runvil-internal/engine/src/execute/scheduler/drive.rs +++ b/crates/runvil-internal/engine/src/execute/scheduler/drive.rs @@ -9,7 +9,7 @@ //! every issued effect has its answer, so every effect the caller was //! handed has exactly one answer. -use runvil_types::error::{ErrorCode, place}; +use runvil_types::error::{ErrorCode, failure, place}; use runvil_types::event::lifecycle; use runvil_types::ids::AbandonReason; use runvil_vfs::Origin; @@ -173,7 +173,7 @@ impl Scheduler { prompt.file(), crate::prompt::detail::key_line(prompt, "runvil"), )), - None => Some(crate::prompt::detail::parse_error( + None => Some(failure( ErrorCode::StructureInvalid, "not a runvil prompt: no runvil version", )), diff --git a/crates/runvil-internal/lua/src/coro.rs b/crates/runvil-internal/lua/src/coro.rs index b9330a839..07049f854 100644 --- a/crates/runvil-internal/lua/src/coro.rs +++ b/crates/runvil-internal/lua/src/coro.rs @@ -31,7 +31,7 @@ use runvil_model_client::model::ModelBinding; use crate::engine_globals::route_store_to_shims; use crate::error::{self, Result}; -use crate::error_value::{install_error_value, install_normalize_failure}; +use crate::error_value::{install_error_value, normalized}; use crate::hardening::CancelHook; use crate::models::{LuaModelHandle, selection_poisoned}; use crate::program::LuaProgram; @@ -171,7 +171,7 @@ pub(crate) fn install_shims( let compactors: Table = globals.raw_get("compactors").map_err(error::lua)?; let error_value = install_error_value(lua).map_err(error::lua)?; let stash_failure = stash_failure_capture(lua)?; - let normalize_failure = install_normalize_failure(lua).map_err(error::lua)?; + let normalize_failure = lua.create_function(normalized).map_err(error::lua)?; let (enter_adhoc_handler, leave_adhoc_handler) = adhoc_handler_captures(lua, adhoc_handler_depth)?; let hook = cancel_hook.clone(); diff --git a/crates/runvil-internal/lua/src/error-value-classify.rs b/crates/runvil-internal/lua/src/error-value-classify.rs index ea231177f..9303d2760 100644 --- a/crates/runvil-internal/lua/src/error-value-classify.rs +++ b/crates/runvil-internal/lua/src/error-value-classify.rs @@ -3,7 +3,7 @@ use std::num::NonZeroU32; -use mlua::{Function, Lua, Value}; +use mlua::{Lua, Value}; use runvil_types::error::Error; use super::error_table; @@ -64,18 +64,6 @@ fn traceback_line(error: &mlua::Error) -> Option { } } -/// Builds the `normalize_failure(value)` chunk capture for the shim's -/// `pcall` and `xpcall` replacements: a Rust callback's failure (mlua's -/// wrapped error) becomes the error table its classification names; any -/// other caught value - a string, an author's own table, an error table -/// already built - passes through unchanged. -/// -/// # Errors -/// Returns an `mlua` error if the function cannot be created. -pub(crate) fn install_normalize_failure(lua: &Lua) -> mlua::Result { - lua.create_function(normalized) -} - /// Returns what `normalize_failure` returns for `value`: a Rust callback's /// failure as the error table its classification names, anything else /// unchanged, for the capture and the loop's compactor rule alike. diff --git a/crates/runvil-internal/lua/src/error-value.rs b/crates/runvil-internal/lua/src/error-value.rs index cd6cad6e3..08fa02bc4 100644 --- a/crates/runvil-internal/lua/src/error-value.rs +++ b/crates/runvil-internal/lua/src/error-value.rs @@ -16,7 +16,7 @@ //! //! [`ErrorKind`] names the closed vocabulary of kinds; [`error_kind`] and //! [`error_fields`] render a universal error into the shape; -//! [`install_normalize_failure`] is the capture the shim's `pcall` and +//! [`normalized`] is the capture the shim's `pcall` and //! `xpcall` replacements run a caught value through (so a Rust callback's //! failure, which mlua raises as an opaque userdata, takes the same shape); //! and [`raised_from`] reads a table back into a universal error when a @@ -42,7 +42,7 @@ use runvil_types::error::{Error, ErrorCode, FactKey, coordinates}; use crate::error::accepted_tags; -pub(crate) use classify::{install_normalize_failure, normalized}; +pub(crate) use classify::normalized; pub(crate) use raised::raised_from; pub use store::store_error; #[cfg(any(test, feature = "test-support"))] diff --git a/crates/runvil-internal/prompt/src/detail.rs b/crates/runvil-internal/prompt/src/detail.rs index cd8470592..030bd064a 100644 --- a/crates/runvil-internal/prompt/src/detail.rs +++ b/crates/runvil-internal/prompt/src/detail.rs @@ -6,19 +6,10 @@ use std::num::NonZeroU32; -use runvil_types::error::{Error, ErrorCode}; +use runvil_types::error::Error; use crate::{Block, Prompt, Section}; -/// Builds a parse failure with no location, for a failure the Engine -/// raises about a parsed prompt. -/// -/// Its prose is the code's default until [`with_prompt_context`] writes it. -#[must_use] -pub fn parse_error(code: ErrorCode, message: impl Into) -> Error { - crate::error::failure(code, message) -} - /// Gives a parse failure the Engine raises about a parsed prompt the /// prompt's frontmatter `name` as its `Name` fact, its place in the /// prompt's `file`, and the prose a failure the parse returns has. diff --git a/crates/runvil-internal/vfs/src/lib.rs b/crates/runvil-internal/vfs/src/lib.rs index 933d7f9e7..89a7a784e 100644 --- a/crates/runvil-internal/vfs/src/lib.rs +++ b/crates/runvil-internal/vfs/src/lib.rs @@ -38,7 +38,7 @@ use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; pub use error::{ParsePathReasonError, PathReason, VfsError}; pub use handle::{Access, VfsRef}; pub use memory::MemoryBackend; -pub use observe::{OpEvent, OpSink, Origin}; +pub use observe::{OpEvent, Origin}; pub use path::{VfsPath, VfsPathBuf}; pub use real::{RealBackend, Writability}; pub use router::VfsRefBuilder; diff --git a/crates/runvil-internal/vfs/src/observe.rs b/crates/runvil-internal/vfs/src/observe.rs index 13c09167e..d6ebe3b0e 100644 --- a/crates/runvil-internal/vfs/src/observe.rs +++ b/crates/runvil-internal/vfs/src/observe.rs @@ -109,7 +109,7 @@ impl<'a> OpEvent<'a> { /// /// The sink must be cheap. Store operations call it from the blocking /// pool, inline with the operation. -pub type OpSink = Arc) + Send + Sync>; +pub(crate) type OpSink = Arc) + Send + Sync>; #[cfg(test)] mod tests { diff --git a/vibe/2026-11-06-1-engine-surface-trims.md b/vibe/2026-11-06-1-engine-surface-trims.md new file mode 100644 index 000000000..767411758 --- /dev/null +++ b/vibe/2026-11-06-1-engine-surface-trims.md @@ -0,0 +1,157 @@ +--- +name: Engine surface trims +overview: Remove three pieces of Engine surface that carry no weight - a public type alias nothing outside its crate names, a one-line forwarder in the prompt crate's `detail` module, and a one-line wrapper with one caller in the Lua crate - with no change in behavior. +todos: + - id: op-sink + content: "Stop exporting the `OpSink` alias from `runvil-vfs` and narrow it to `pub(crate)`, in its own commit" + status: completed + - id: parse-error + content: "Delete `runvil_prompt::detail::parse_error` and point its two callers at `runvil_types::error::failure`, in its own commit" + status: completed + - id: normalize-failure + content: "Inline `install_normalize_failure` into `install_shims` in `runvil-lua`, in its own commit, and finish with the full gate set" + status: completed +isProject: false +--- + +# Engine surface trims + + + +## Product Requirements + +Three items in the Engine crates exist only to be named once. Removing them shrinks what a reviewer reads in `runvil-vfs`, `runvil-prompt`, and `runvil-lua` and changes nothing a run does. No replacement facility is needed for any of them. + +- Problem and users: + - `runvil-vfs` exports `OpSink`, a type alias for `Arc) + Send + Sync>`. No crate outside `runvil-vfs` names it, in production or in tests, and the facade does not re-export it. The public builder takes `impl Fn(OpEvent<'_>)`. + - `runvil_prompt::detail::parse_error` is one call to `runvil_types::error::failure` with its two arguments passed on unchanged. It has one production caller, `Scheduler::check_start`, and one test. + - `install_normalize_failure` in `runvil-lua` is `lua.create_function(normalized)`. It has one caller, `install_shims` in `coro.rs`. + - The user is the reviewer reading each crate's surface (`AGENTS.md`, Principles). +- Goals: + - `OpSink` is nameable only inside `runvil-vfs`. + - `prompt::detail` holds one item fewer and the Engine calls `runvil_types::error::failure` itself. + - `install_shims` creates the `normalize_failure` capture directly from `normalized`. +- Non-goals: + - Gating test-only exports of `runvil-lua` (`ErrorField`, `error_fields`, `RESERVED_NAMES`) behind `test-support`. + - Any change to `runvil-types`. + - Inlining `block_guard` (see the Decision Record). + - Renaming anything, or any change to behavior, a persisted format, or the facade. +- Success criteria: + - `crates/runvil/public-api.txt` does not change. + - Every gate in `AGENTS.md` "Verification" passes with no new warning. +- Constraints: + - Each change is its own commit and leaves the workspace green. +- Open questions: None + +## Functional Specification + +No behavior changes. Every failure, event, and Lua value is identical before and after. + +- Inputs and outputs: none change. +- States and validation: none change. +- Errors and recovery: `check_start` still returns a `StructureInvalid` failure with the same message and no location, stamped by `with_prompt_context` as before. +- Security and privacy behavior: none change. + + + + +## Technical Design + +Each change deletes a name and points its users at what the name already forwarded to. + +- Architecture: + - `OpSink` stays as a `pub(crate)` alias in `observe.rs`, because `Volume`, `VfsRef::from_router`, and `VfsRefBuilder` name it. Only the root re-export goes. + - `parse_error` goes. `check_start` and its test call `runvil_types::error::failure`, which `runvil-engine` already imports in `engine/src/error.rs`. + - `install_normalize_failure` goes. `install_shims` calls `lua.create_function(normalized)`, and the `error_value` module doc names `normalized` where it named the installer. +- Data model: none. +- Interfaces and APIs: `runvil_vfs::OpSink` and `runvil_prompt::detail::parse_error` leave the crates' surfaces. Neither is in `crates/runvil/public-api.txt`. +- Failure handling: unchanged. +- Observability: unchanged. +- Performance and scale: unchanged. + + + + +## Testing Plan + +No new test, because no behavior changes. The existing tests cover each call site: the vfs observer tests for `OpSink`, `a_parse_failure_run_raises_names_the_prompt_and_its_file` for `parse_error`, and the Lua prelude and `pcall` tests for the `normalize_failure` capture. + +- Unit: unchanged. +- Exit criteria: + - `cargo clippy --workspace --exclude workshop --exclude workshop-server --exclude workshop-server-api --all-targets --all-features` and the workshop clippy run, both with `CARGO_BUILD_WARNINGS=deny` + - `cargo fmt --all --check` + - `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features --exclude workshop --exclude workshop-server --exclude workshop-server-api` + - `RUSTDOCFLAGS="-D warnings" cargo doc -p runvil --no-deps` + - `cargo nextest run --locked --workspace --exclude workshop --exclude workshop-server --exclude workshop-server-api --all-features` + - `cargo nextest run --locked -p workshop -p workshop-server -p workshop-server-api` + - `cargo +nightly-2026-09-05 xtask api --check` + - `cargo test -p build-xtask` + - `node --test crates/workshop/ui/test/docs-claims.mjs` + + + + +## Decision Record + +- Decisions: + - Narrow `OpSink` to `pub(crate)` instead of deleting it. Rationale: three in-crate items name it, and a named alias is shorter than the closure type at each of them. + - Do not inline `block_guard`. Rationale: `GUARD_REGISTRY` is private to `coro.rs`, which both stores the guard (`install_shims`) and reads it back (`block_guard`). Inlining at the one caller in `vm/run.rs` would make the key `pub(crate)` and split the pair across two modules, to save a three-line function. That is not a clear win. + - Reuse considered: all three removals reuse the facility the removed name pointed to (`runvil_types::error::failure`, `normalized`, the `impl Fn` builder). No new facility. + +### Deferred and Out of Scope + +- Deferred: moving `ErrorField`, `error_fields`, and `RESERVED_NAMES` behind `test-support`. The definitions need the same `cfg` as the export, because dead code fails the deny-warnings gate, so it is a larger change than a trim. +- Deferred: `runvil-types`, where another engineer is proposing work. +- Deferred: `block_guard`, for the reason above. + + + + +## Execution Instructions + + + +### Step 1: Stop exporting OpSink [completed] + +- Component: `op-sink` +- Tests: + - `cargo clippy -p runvil-vfs --all-targets --all-features` + - `RUSTDOCFLAGS="-D warnings" cargo doc -p runvil --no-deps` + +Build: in `crates/runvil-internal/vfs/src/observe.rs` change `pub type OpSink` to `pub(crate) type OpSink`, and in `lib.rs` drop `OpSink` from `pub use observe::{OpEvent, OpSink, Origin};`. + +Commit: one commit. Suggested subject: ``Stop exporting `OpSink` from `runvil-vfs` ``. + + + + + +### Step 2: Delete prompt::detail::parse_error [completed] + +- Component: `parse-error` +- Tests: + - `cargo nextest run --locked -p runvil-engine -p runvil-prompt --all-features` + - `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features --exclude workshop --exclude workshop-server --exclude workshop-server-api` + +Build: delete `parse_error` from `crates/runvil-internal/prompt/src/detail.rs`, dropping `ErrorCode` from that file's imports if nothing else uses it. In `engine/src/execute/scheduler/drive.rs` (`check_start`) and `engine/src/error/tests-chain.rs`, call `runvil_types::error::failure` in its place. + +Commit: one commit. Suggested subject: ``Delete `prompt::detail::parse_error` ``. + + + + + +### Step 3: Inline install_normalize_failure [completed] + +- Component: `normalize-failure` +- Tests: + - `cargo nextest run --locked -p runvil-lua --all-features` + - The full gate set in the Testing Plan + +Build: delete `install_normalize_failure` from `crates/runvil-internal/lua/src/error-value-classify.rs` and its `Function` import, and re-export only `normalized` from `error-value.rs`. In `coro.rs` build the capture with `lua.create_function(normalized).map_err(error::lua)?`. In the `error_value` module doc name `normalized` where it named the installer. + +Commit: one commit. Suggested subject: ``Inline `install_normalize_failure` into `install_shims` ``. + + + +