refactor(tools): rename rlm_process → rlm, rlm_query → parallel_fanout

Two top-level tools shared the rlm_ prefix but did completely different
things — rlm_query was a flat parallel-completion fan-out wearing an
RLM-shaped name, and rlm_process was the actual recursive language model.
The overlap was the source of the "our rlm query is completely wrong"
confusion.

  rlm_process  → rlm              # single, honest name for the recursive tool
  rlm_query    → parallel_fanout  # honest name for the flat fanout

Internal renames follow:
  Op::RlmQuery       → Op::Rlm
  AppAction::RlmQuery → AppAction::Rlm
  handle_rlm_query    → handle_rlm
  RlmProcessTool      → RlmTool
  RlmQueryTool        → ParallelFanoutTool
  RlmChildClient      → FanoutChildClient
  with_rlm_process_tool → with_rlm_tool
  with_rlm_query_tool   → with_parallel_fanout_tool

The REPL helpers `rlm_query` / `rlm_query_batched` / `llm_query` /
`llm_query_batched` keep their names — those are correctly named (they
ARE recursive within the REPL) and the model knows them from the system
prompt and metadata.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hunter Bown
2026-04-27 02:10:17 -05:00
parent 2865c9a766
commit 9dd0d12cea
16 changed files with 220 additions and 70 deletions
+1 -1
View File
@@ -464,7 +464,7 @@ pub fn rlm(app: &mut App, arg: Option<&str>) -> CommandResult {
child_model,
max_depth,
),
AppAction::RlmQuery {
AppAction::Rlm {
prompt,
model,
child_model,
+7 -7
View File
@@ -412,8 +412,8 @@ fn should_default_defer_tool(name: &str, mode: AppMode) -> bool {
| "grep_files"
| "file_search"
| "diagnostics"
| "rlm_query"
| "rlm_process"
| "parallel_fanout"
| "rlm"
| MULTI_TOOL_PARALLEL_NAME
| "update_plan"
| "todo_write"
@@ -1312,13 +1312,13 @@ impl Engine {
Op::CompactContext => {
self.handle_manual_compaction().await;
}
Op::RlmQuery {
Op::Rlm {
content,
model,
child_model,
max_depth,
} => {
self.handle_rlm_query(content, model, child_model, max_depth)
self.handle_rlm(content, model, child_model, max_depth)
.await;
}
Op::Shutdown => {
@@ -1450,8 +1450,8 @@ impl Engine {
builder = builder
.with_review_tool(self.deepseek_client.clone(), self.session.model.clone())
.with_rlm_query_tool(self.deepseek_client.clone())
.with_rlm_process_tool(self.deepseek_client.clone(), self.session.model.clone())
.with_parallel_fanout_tool(self.deepseek_client.clone())
.with_rlm_tool(self.deepseek_client.clone(), self.session.model.clone())
.with_user_input_tool()
.with_parallel_tool();
@@ -1663,7 +1663,7 @@ impl Engine {
/// only sees metadata about the REPL state, never the prompt text
/// directly. The model generates Python code, which is executed by
/// the REPL. When FINAL() is called, the loop ends.
async fn handle_rlm_query(
async fn handle_rlm(
&mut self,
content: String,
model: String,
+2 -2
View File
@@ -67,8 +67,8 @@ pub enum Op {
/// Run a Recursive Language Model (RLM) turn per Algorithm 1 of
/// Zhang et al. (arXiv:2512.24601). The prompt is stored in the REPL
/// as the `PROMPT` variable; the root LLM only sees metadata.
RlmQuery {
/// as `context`; the root LLM only sees metadata.
Rlm {
/// The user's prompt — stored in REPL, NOT in the LLM context.
content: String,
/// The model to use for root LLM calls.
+1 -1
View File
@@ -1,6 +1,6 @@
## Mode: agent
Read-only tools (reads, searches, `rlm_query`, agent status queries, git inspection) run silently.
Read-only tools (reads, searches, `parallel_fanout`, agent status queries, git inspection) run silently.
Any write, patch, shell execution, sub-agent spawn, or CSV batch operation will ask for approval first.
Before requesting approval for writes, lay out your work with `todo_write` so the user can see what
+2 -2
View File
@@ -9,7 +9,7 @@ Your default workflow for any non-trivial request:
2. **Execute** — work through each todo, updating status as you go.
3. **For complex initiatives**, layer `update_plan` (high-level strategy) above `todo_write` (granular steps).
4. **For parallel work**, spawn sub-agents (`agent_spawn` / `agent_swarm`) — each does one thing well. Link them to plan/todo items in your thinking.
5. **For LM-only fan-out** (summarization, classification, analysis across many items), use `rlm_query` for fast parallel inference.
5. **For LM-only fan-out** (summarization, classification, analysis across many items), use `parallel_fanout` for fast parallel inference.
6. **For persistent cross-session memory**, use `note` sparingly for important decisions, open blockers, and architectural context.
**Key principle**: make your work visible. The sidebar shows Plan / Todos / Tasks / Agents. When these panels are empty, the user has no idea what you're doing. Keep them populated.
@@ -28,7 +28,7 @@ Model notes: DeepSeek V4 models emit *thinking tokens* (`ContentBlock::Thinking`
- **Git / diag / tests**: `git_status`, `git_diff`, `git_show`, `git_log`, `git_blame`, `diagnostics`, `run_tests`, `review`.
- **Sub-agents**: `agent_spawn` (`spawn_agent`, `delegate_to_agent`), `agent_swarm`, `agent_result`, `agent_cancel` (`close_agent`), `agent_list`, `agent_wait` (`wait`), `agent_send_input` (`send_input`), `agent_assign` (`assign_agent`), `resume_agent`.
- **CSV batch**: `spawn_agents_on_csv`, `report_agent_job_result`.
- **LM fan-out**: `rlm_query` — `prompts: [...]` runs up to 16 children on the fast cheap model concurrently. Read-only.
- **LM fan-out**: `parallel_fanout` — `prompts: [...]` runs up to 16 children on the fast cheap model concurrently. Read-only.
- **Other**: `code_execution` (Python sandbox), `validate_data` (JSON/TOML), `request_user_input`, `finance` (market quotes), `tool_search_tool_regex`, `tool_search_tool_bm25` (deferred tool discovery).
Multiple `tool_calls` in one turn run in parallel. `web_search` returns `ref_id`s — cite as `(ref_id)`.
+1 -1
View File
@@ -1,6 +1,6 @@
## Mode: normal
Reads and `rlm_query` run silently. Writes, patches, and shell commands ask for approval.
Reads and `parallel_fanout` run silently. Writes, patches, and shell commands ask for approval.
Before requesting writes, use `todo_write` to outline your approach — visible plans build trust.
For complex work, layer `update_plan` (strategy) above `todo_write` (tactics).
+2 -2
View File
@@ -14,8 +14,8 @@ pub mod plan;
pub mod project;
pub mod registry;
pub mod review;
pub mod rlm_process;
pub mod rlm_query;
pub mod rlm;
pub mod parallel_fanout;
pub mod search;
pub mod shell;
mod shell_output;
@@ -34,7 +34,7 @@ const MAX_PARALLEL: usize = 16;
const DEFAULT_CHILD_TIMEOUT: Duration = Duration::from_secs(120);
// ---------------------------------------------------------------------------
// RlmChildClient — dyn-compatible wrapper around LLM completion.
// FanoutChildClient — dyn-compatible wrapper around LLM completion.
//
// The workspace's `LlmClient` trait uses native `async fn`, which is not dyn
// compatible in stable Rust (RPITIT vtable limitations). We define a small
@@ -47,13 +47,13 @@ const DEFAULT_CHILD_TIMEOUT: Duration = Duration::from_secs(120);
/// operation. `#[async_trait]` desugars the async method into a boxed future
/// so the trait is object-safe.
#[async_trait]
pub(crate) trait RlmChildClient: Send + Sync {
pub(crate) trait FanoutChildClient: Send + Sync {
async fn complete(&self, request: MessageRequest) -> anyhow::Result<MessageResponse>;
}
/// Blanket impl: any `DeepSeekClient` is a valid child client.
#[async_trait]
impl RlmChildClient for DeepSeekClient {
impl FanoutChildClient for DeepSeekClient {
async fn complete(&self, request: MessageRequest) -> anyhow::Result<MessageResponse> {
self.create_message(request).await
}
@@ -61,28 +61,28 @@ impl RlmChildClient for DeepSeekClient {
/// Tool: `rlm_query`. Runs one or more prompts in parallel and joins the
/// results. Structured tool call so the model can trigger fan-out reliably.
pub struct RlmQueryTool {
/// Boxed child client — `Arc<dyn RlmChildClient>` lets tests inject a
pub struct ParallelFanoutTool {
/// Boxed child client — `Arc<dyn FanoutChildClient>` lets tests inject a
/// mock without going through a real HTTP connection. `None` when no API
/// key is configured.
client: Option<Arc<dyn RlmChildClient>>,
client: Option<Arc<dyn FanoutChildClient>>,
default_model: String,
}
impl RlmQueryTool {
impl ParallelFanoutTool {
/// Construct with a concrete `DeepSeekClient` (production path).
#[must_use]
pub fn new(client: Option<DeepSeekClient>) -> Self {
Self {
client: client.map(|c| Arc::new(c) as Arc<dyn RlmChildClient>),
client: client.map(|c| Arc::new(c) as Arc<dyn FanoutChildClient>),
default_model: DEFAULT_CHILD_MODEL.to_string(),
}
}
/// Construct with a pre-boxed `RlmChildClient` — used by tests to inject
/// Construct with a pre-boxed `FanoutChildClient` — used by tests to inject
/// a `MockRlmClient` without an active API connection.
#[cfg(test)]
pub(crate) fn new_with_arc(client: Option<Arc<dyn RlmChildClient>>) -> Self {
pub(crate) fn new_with_arc(client: Option<Arc<dyn FanoutChildClient>>) -> Self {
Self {
client,
default_model: DEFAULT_CHILD_MODEL.to_string(),
@@ -91,9 +91,9 @@ impl RlmQueryTool {
}
#[async_trait]
impl ToolSpec for RlmQueryTool {
impl ToolSpec for ParallelFanoutTool {
fn name(&self) -> &'static str {
"rlm_query"
"parallel_fanout"
}
fn description(&self) -> &'static str {
@@ -178,16 +178,16 @@ impl ToolSpec for RlmQueryTool {
};
if prompts.is_empty() {
return Err(ToolError::invalid_input("rlm_query: prompts list is empty"));
return Err(ToolError::invalid_input("parallel_fanout: prompts list is empty"));
}
if prompts.len() > MAX_PARALLEL {
return Err(ToolError::invalid_input(format!(
"rlm_query: too many prompts ({}, max {MAX_PARALLEL})",
"parallel_fanout: too many prompts ({}, max {MAX_PARALLEL})",
prompts.len(),
)));
}
// client is already Arc<dyn RlmChildClient> — clone the Arc, not the client.
// client is already Arc<dyn FanoutChildClient> — clone the Arc, not the client.
let model = Arc::new(model);
let system = Arc::new(system);
let total = prompts.len();
@@ -211,7 +211,7 @@ impl ToolSpec for RlmQueryTool {
peak.fetch_max(now, Ordering::Relaxed);
debug!(
target: "deepseek_cli::tools",
tool = "rlm_query",
tool = "parallel_fanout",
idx,
in_flight = now,
"child request start"
@@ -260,7 +260,7 @@ impl ToolSpec for RlmQueryTool {
debug!(
target: "deepseek_cli::tools",
tool = "rlm_query",
tool = "parallel_fanout",
idx,
elapsed_ms,
ok = response.is_ok(),
@@ -277,7 +277,7 @@ impl ToolSpec for RlmQueryTool {
let dispatch_elapsed_ms = dispatch_started.elapsed().as_millis() as u64;
debug!(
target: "deepseek_cli::tools",
tool = "rlm_query",
tool = "parallel_fanout",
total,
peak = peak.load(Ordering::Relaxed),
dispatch_elapsed_ms,
@@ -372,8 +372,8 @@ mod tests {
)
}
fn tool_without_client() -> RlmQueryTool {
RlmQueryTool::new(None)
fn tool_without_client() -> ParallelFanoutTool {
ParallelFanoutTool::new(None)
}
// -----------------------------------------------------------------------
@@ -402,7 +402,7 @@ mod tests {
}
#[async_trait]
impl RlmChildClient for MockRlmClient {
impl FanoutChildClient for MockRlmClient {
async fn complete(&self, request: MessageRequest) -> anyhow::Result<MessageResponse> {
// Record start time before sleeping.
self.start_times.lock().unwrap().push(Instant::now());
@@ -459,7 +459,7 @@ mod tests {
let mock = Arc::new(MockRlmClient::new(delay));
let start_times_ref = Arc::clone(&mock.start_times);
let tool = RlmQueryTool::new_with_arc(Some(mock as Arc<dyn RlmChildClient>));
let tool = ParallelFanoutTool::new_with_arc(Some(mock as Arc<dyn FanoutChildClient>));
let prompts: Vec<&str> = vec!["a", "b", "c", "d"];
let overall_start = Instant::now();
@@ -511,7 +511,7 @@ mod tests {
#[tokio::test]
async fn rlm_single_prompt_returns_plain_text() {
let mock = Arc::new(MockRlmClient::new(std::time::Duration::from_millis(1)));
let tool = RlmQueryTool::new_with_arc(Some(mock as Arc<dyn RlmChildClient>));
let tool = ParallelFanoutTool::new_with_arc(Some(mock as Arc<dyn FanoutChildClient>));
let result = tool
.execute(json!({ "prompt": "hello" }), &ctx())
@@ -527,7 +527,7 @@ mod tests {
#[tokio::test]
async fn rlm_multi_prompt_returns_indexed_blocks() {
let mock = Arc::new(MockRlmClient::new(std::time::Duration::from_millis(1)));
let tool = RlmQueryTool::new_with_arc(Some(mock as Arc<dyn RlmChildClient>));
let tool = ParallelFanoutTool::new_with_arc(Some(mock as Arc<dyn FanoutChildClient>));
let result = tool
.execute(json!({ "prompts": ["alpha", "beta"] }), &ctx())
+6 -6
View File
@@ -384,18 +384,18 @@ impl ToolRegistryBuilder {
/// Include the native RLM tool (`rlm_query`). Parallel/batched LLM
/// fan-out runs through the existing DeepSeek client.
#[must_use]
pub fn with_rlm_query_tool(self, client: Option<DeepSeekClient>) -> Self {
use super::rlm_query::RlmQueryTool;
self.with_tool(Arc::new(RlmQueryTool::new(client)))
pub fn with_parallel_fanout_tool(self, client: Option<DeepSeekClient>) -> Self {
use super::parallel_fanout::ParallelFanoutTool;
self.with_tool(Arc::new(ParallelFanoutTool::new(client)))
}
/// Include the heavy-lift RLM tool (`rlm_process`). Runs the full
/// recursive language-model loop on a long input (file or inline
/// content); the long input never enters the calling model's context.
#[must_use]
pub fn with_rlm_process_tool(self, client: Option<DeepSeekClient>, root_model: String) -> Self {
use super::rlm_process::RlmProcessTool;
self.with_tool(Arc::new(RlmProcessTool::new(client, root_model)))
pub fn with_rlm_tool(self, client: Option<DeepSeekClient>, root_model: String) -> Self {
use super::rlm::RlmTool;
self.with_tool(Arc::new(RlmTool::new(client, root_model)))
}
/// Include the review tool.
@@ -31,7 +31,7 @@ const DEFAULT_MAX_DEPTH: u32 = 1;
/// context in the first place.
const MAX_INLINE_CONTENT_CHARS: usize = 200_000;
pub struct RlmProcessTool {
pub struct RlmTool {
/// Production HTTP client. `None` when no API key is configured.
client: Option<DeepSeekClient>,
/// Root model to drive the RLM loop. Set at registration time; matches
@@ -39,7 +39,7 @@ pub struct RlmProcessTool {
root_model: String,
}
impl RlmProcessTool {
impl RlmTool {
#[must_use]
pub fn new(client: Option<DeepSeekClient>, root_model: String) -> Self {
Self { client, root_model }
@@ -47,9 +47,9 @@ impl RlmProcessTool {
}
#[async_trait]
impl ToolSpec for RlmProcessTool {
impl ToolSpec for RlmTool {
fn name(&self) -> &'static str {
"rlm_process"
"rlm"
}
fn description(&self) -> &'static str {
@@ -101,7 +101,7 @@ impl ToolSpec for RlmProcessTool {
}
fn approval_requirement(&self) -> ApprovalRequirement {
// Same level as rlm_query: the model decided to invoke this, the
// Same level as parallel_fanout: the model decided to invoke this, the
// user already enabled tools by being in Agent/YOLO mode, and
// every concrete side-effect (file read, LLM call) is bounded.
ApprovalRequirement::Auto
@@ -128,7 +128,7 @@ impl ToolSpec for RlmProcessTool {
})?
.trim();
if task.is_empty() {
return Err(ToolError::invalid_input("rlm_process: `task` is empty"));
return Err(ToolError::invalid_input("rlm: `task` is empty"));
}
let file_path = input.get("file_path").and_then(|v| v.as_str());
@@ -137,12 +137,12 @@ impl ToolSpec for RlmProcessTool {
let body = match (file_path, content) {
(Some(_), Some(_)) => {
return Err(ToolError::invalid_input(
"rlm_process: pass `file_path` OR `content`, not both",
"rlm: pass `file_path` OR `content`, not both",
));
}
(None, None) => {
return Err(ToolError::invalid_input(
"rlm_process: requires `file_path` (preferred) or `content`",
"rlm: requires `file_path` (preferred) or `content`",
));
}
(Some(path), None) => {
@@ -156,7 +156,7 @@ impl ToolSpec for RlmProcessTool {
(None, Some(c)) => {
if c.chars().count() > MAX_INLINE_CONTENT_CHARS {
return Err(ToolError::invalid_input(format!(
"rlm_process: inline `content` is {} chars (cap {MAX_INLINE_CONTENT_CHARS}). Pass `file_path` for larger inputs.",
"rlm: inline `content` is {} chars (cap {MAX_INLINE_CONTENT_CHARS}). Pass `file_path` for larger inputs.",
c.chars().count()
)));
}
@@ -166,7 +166,7 @@ impl ToolSpec for RlmProcessTool {
if body.trim().is_empty() {
return Err(ToolError::invalid_input(
"rlm_process: input is empty after loading",
"rlm: input is empty after loading",
));
}
@@ -208,7 +208,7 @@ impl ToolSpec for RlmProcessTool {
if let Some(err) = result.error {
return Err(ToolError::ExecutionFailed {
message: format!(
"rlm_process: {err} (iterations={}, termination={:?})",
"rlm: {err} (iterations={}, termination={:?})",
result.iterations, result.termination
),
});
@@ -217,7 +217,7 @@ impl ToolSpec for RlmProcessTool {
if result.answer.trim().is_empty() {
return Err(ToolError::ExecutionFailed {
message: format!(
"rlm_process: empty answer (termination={:?}, iterations={})",
"rlm: empty answer (termination={:?}, iterations={})",
result.termination, result.iterations
),
});
@@ -303,8 +303,8 @@ impl ToolSpec for RlmProcessTool {
mod tests {
use super::*;
fn tool() -> RlmProcessTool {
RlmProcessTool::new(None, "deepseek-v4-pro".to_string())
fn tool() -> RlmTool {
RlmTool::new(None, "deepseek-v4-pro".to_string())
}
fn ctx() -> ToolContext {
@@ -321,7 +321,7 @@ mod tests {
#[test]
fn name_and_schema() {
let t = tool();
assert_eq!(t.name(), "rlm_process");
assert_eq!(t.name(), "rlm");
let schema = t.input_schema();
assert!(schema["properties"]["task"].is_object());
assert!(schema["properties"]["file_path"].is_object());
@@ -362,7 +362,7 @@ mod tests {
#[tokio::test]
async fn rejects_missing_task() {
let t = RlmProcessTool::new(None, "x".into());
let t = RlmTool::new(None, "x".into());
let ctx = ctx();
let res = t
.execute(json!({"content": "abc"}), &ctx)
+1 -1
View File
@@ -1927,7 +1927,7 @@ pub enum AppAction {
/// Run a Recursive Language Model (RLM) turn — Algorithm 1 from
/// Zhang et al. (arXiv:2512.24601). The prompt is stored in the REPL;
/// the root LLM only sees metadata.
RlmQuery {
Rlm {
/// The user's prompt — stored in REPL, NOT in LLM context.
prompt: String,
/// Model for the root LLM.
+1 -1
View File
@@ -2229,7 +2229,7 @@ mod tests {
// its own row instead of the inline `args:` summary so the user can
// read what each child was asked.
let cell = HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
name: "rlm_query".to_string(),
name: "parallel_fanout".to_string(),
status: ToolStatus::Running,
input_summary: Some("prompts: <3 items>".to_string()),
output: None,
+3 -3
View File
@@ -2337,7 +2337,7 @@ async fn apply_command_result(
let queued = build_queued_message(app, content);
submit_or_steer_message(app, engine_handle, queued).await?;
}
AppAction::RlmQuery {
AppAction::Rlm {
prompt,
model,
child_model,
@@ -2345,7 +2345,7 @@ async fn apply_command_result(
} => {
app.status_message = Some("RLM turn starting...".to_string());
let _ = engine_handle
.send(Op::RlmQuery {
.send(Op::Rlm {
content: prompt,
model,
child_model,
@@ -4610,7 +4610,7 @@ fn handle_tool_call_started(app: &mut App, id: &str, name: &str, input: &serde_j
/// user can read what each child was asked. Returns `None` for tools that
/// don't expose a prompt list.
fn extract_fanout_prompts(name: &str, input: &serde_json::Value) -> Option<Vec<String>> {
if name != "rlm_query" {
if name != "parallel_fanout" {
return None;
}
if let Some(arr) = input.get("prompts").and_then(|v| v.as_array()) {
+3 -3
View File
@@ -1862,7 +1862,7 @@ fn rlm_query_tool_cell_wired_with_prompts_on_start() {
handle_tool_call_started(
&mut app,
"rlm-1",
"rlm_query",
"parallel_fanout",
&serde_json::json!({
"prompts": [
"What is the capital of France?",
@@ -1878,7 +1878,7 @@ fn rlm_query_tool_cell_wired_with_prompts_on_start() {
panic!("expected GenericToolCell for rlm_query");
};
assert_eq!(generic.name, "rlm_query");
assert_eq!(generic.name, "parallel_fanout");
assert_eq!(generic.status, ToolStatus::Running);
// Core assertion: prompts populated from the JSON input.
@@ -1902,7 +1902,7 @@ fn rlm_query_singular_prompt_wired_as_single_element_vec() {
handle_tool_call_started(
&mut app,
"rlm-2",
"rlm_query",
"parallel_fanout",
&serde_json::json!({ "prompt": "Explain the engine loop" }),
);
+99
View File
@@ -0,0 +1,99 @@
# Product Catalog - Winter 2025
## Electronics
### SKU-001: QuantumBook Pro Laptop
- Price: $1,299.99
- Category: Computing
- Stock: 45 units
- Rating: 4.7/5
- Features: 16" 4K display, 32GB RAM, 1TB SSD, AI accelerator
- Warranty: 2 years
- Supplier: TechGlobal Inc.
### SKU-002: SmartWatch X5
- Price: $349.99
- Category: Wearables
- Stock: 120 units
- Rating: 4.3/5
- Features: Heart rate, ECG, GPS, 7-day battery
- Warranty: 1 year
- Supplier: GadgetWorld Ltd.
### SKU-003: NoiseCancel Pro Headphones
- Price: $199.99
- Category: Audio
- Stock: 8 units (LOW STOCK)
- Rating: 4.9/5
- Features: ANC, 40hr battery, spatial audio
- Warranty: 1 year
- Supplier: AudioTech Corp.
## Home & Kitchen
### SKU-004: SmartBrew Coffee Maker
- Price: $89.99
- Category: Kitchen Appliances
- Stock: 200 units
- Rating: 4.5/5
- Features: App-controlled, 12-cup, thermal carafe
- Warranty: 2 years
- Supplier: HomeEssentials LLC
### SKU-005: AeroChef Air Fryer
- Price: $129.99
- Category: Kitchen Appliances
- Stock: 75 units
- Rating: 4.6/5
- Features: 8qt capacity, 10 presets, dishwasher safe
- Warranty: 1 year
- Supplier: HomeEssentials LLC
### SKU-006: PureFlow Water Filter
- Price: $49.99
- Category: Kitchen Accessories
- Stock: 0 units (OUT OF STOCK)
- Rating: 4.4/5
- Features: 3-stage filtration, 6-month filter life
- Warranty: 1 year
- Supplier: CleanWater Systems
## Sports & Outdoors
### SKU-007: TrailBlazer Hiking Boots
- Price: $159.99
- Category: Footwear
- Stock: 60 units
- Rating: 4.8/5
- Features: Waterproof, Vibram sole, ankle support
- Warranty: 1 year
- Supplier: OutdoorGear Co.
### SKU-008: FlexCore Yoga Mat
- Price: $39.99
- Category: Fitness
- Stock: 300 units
- Rating: 4.2/5
- Features: 6mm thick, non-slip, carrying strap
- Warranty: 6 months
- Supplier: FitLife Products
### SKU-009: PowerLift Adjustable Dumbbells
- Price: $299.99
- Category: Fitness
- Stock: 15 units
- Rating: 4.7/5
- Features: 5-52.5lbs range, quick-change, storage tray
- Warranty: 2 years
- Supplier: FitLife Products
## Automotive
### SKU-010: DashCam Ultra 4K
- Price: $179.99
- Category: Car Electronics
- Stock: 35 units
- Rating: 4.5/5
- Features: 4K recording, night vision, GPS, parking mode
- Warranty: 1 year
- Supplier: AutoGadget Inc.
+51
View File
@@ -0,0 +1,51 @@
This is a test document for rlm_process.
# Security Model Overview
The system uses a zero-trust architecture where every request is authenticated and authorized independently.
## Authentication
Authentication is handled via JWT tokens with a 15-minute expiry. Refresh tokens are stored in an HTTP-only cookie.
## Authorization
Role-based access control (RBAC) is used with three tiers:
- Admin: full access
- Editor: can modify content but not system settings
- Viewer: read-only access
## Encryption
All data at rest is encrypted using AES-256-GCM. Data in transit uses TLS 1.3.
## Audit Logging
Every action is logged to an append-only audit trail stored in a separate database.
# API Endpoints
- GET /api/users - List users (Admin only)
- POST /api/users - Create user (Admin only)
- GET /api/documents - List documents
- POST /api/documents - Create document (Admin, Editor)
- PUT /api/documents/:id - Update document (Admin, Editor)
- DELETE /api/documents/:id - Delete document (Admin only)
# Data Model
User {
id: UUID
email: String
role: Enum(Admin, Editor, Viewer)
created_at: Timestamp
}
Document {
id: UUID
title: String
content: Text
author_id: UUID (FK -> User)
created_at: Timestamp
updated_at: Timestamp
}