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
123 changes: 123 additions & 0 deletions crates/command-contract/src/facets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ pub trait CommandMediaContext {
fn attach_media(&mut self, resolved_path: &Path) -> Result<MediaAttachmentReceipt, String>;
}

// ---------------------------------------------------------------------------
// Project (FEAT-021 D1/D2/D3/D4)
// ---------------------------------------------------------------------------

Expand Down Expand Up @@ -206,6 +207,128 @@ pub trait CommandProjectContext {
fn goal_state(&self) -> ProjectGoalState;
}

// ---------------------------------------------------------------------------
// Memory (FEAT-019 D1/D2/D8/D9)
// ---------------------------------------------------------------------------

/// Portable semantic hit for a native-memory search or get result.
///
/// Carries only the typed location and text the handler consumes for
/// formatting; the TUI-owned `NativeMemoryHit` never crosses the boundary.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemoryHit {
pub source: PathBuf,
pub line_start: usize,
pub line_end: usize,
pub text: String,
}

/// Portable native-memory location summary (status operation).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemoryStatus {
pub root: PathBuf,
pub source: PathBuf,
pub index: PathBuf,
}

/// Portable result of a successful remember operation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemoryRemembered {
pub source: PathBuf,
pub line_start: usize,
}

/// Portable import outcome: imported (with destination) or skipped.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MemoryImportOutcome {
Imported { destination: PathBuf },
Skipped,
}

/// Portable get outcome: found hit or explicit not-found.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MemoryGetOutcome {
Found(MemoryHit),
NotFound,
}

/// Portable export payload — the exported memory document itself, never a
/// preformatted command response.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemoryExport {
pub content: String,
}

/// Portable reindex entry count.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemoryReindex {
pub entry_count: usize,
}

/// Zero-field success value for delete operations (D2): the handler already
/// owns the selected scope and needs no additional success data.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct MemoryDelete;

/// Typed remember target (D9): the handler resolves workspace identity through
/// the workspace facet and passes the resulting typed ID here.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MemoryRememberTarget {
Global,
Workspace { workspace_id: String },
}

/// Typed delete scope for the non-workspace delete method (D8/D9).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemoryDeleteScope {
/// Delete every memory entry (global and all workspace scopes).
All,
/// Delete only the global scope entries.
Global,
}

/// Host memory data for the memory command group (FEAT-019 D1).
///
/// Exposes the resolved user-memory file path, the enablement flag, and one
/// typed method per exposed native-memory operation. All results are
/// contract-owned portable values; implementation errors cross as safe text.
/// Workspace-scoped operations take the borrowed workspace path as their first
/// argument (D8); non-workspace operations never receive workspace authority
/// and the facet never captures or retains workspace state internally.
pub trait CommandMemoryContext {
/// The resolved user-memory file path.
fn memory_path(&self) -> PathBuf;
/// Whether the `[memory] enabled` / `DEEPSEEK_MEMORY=on` flag is set.
fn memory_enabled(&self) -> bool;
/// Native-memory root, global source, and index paths.
fn status(&self) -> Result<MemoryStatus, String>;
/// The native-memory root path.
fn path(&self) -> Result<PathBuf, String>;
/// Workspace identity for the given workspace path.
fn workspace_id(&self, workspace: &Path) -> Result<String, String>;
/// Workspace-scoped search over the native-memory store.
fn search(&self, workspace: &Path, query: &str, limit: usize)
-> Result<Vec<MemoryHit>, String>;
/// Append a reviewed note to the typed global or workspace target.
fn remember(
&self,
target: MemoryRememberTarget,
note: &str,
) -> Result<MemoryRemembered, String>;
/// Import legacy memory; distinguishes imported from skipped.
fn import(&self) -> Result<MemoryImportOutcome, String>;
/// Workspace-scoped get by entry id; not-found is a typed outcome.
fn get(&self, workspace: &Path, id: i64) -> Result<MemoryGetOutcome, String>;
/// Export the native-memory document content.
fn export(&self) -> Result<MemoryExport, String>;
/// Reindex the native-memory store; returns the indexed entry count.
fn reindex(&self) -> Result<MemoryReindex, String>;
/// Delete all or global scope; never receives workspace authority.
fn delete(&self, scope: MemoryDeleteScope) -> Result<MemoryDelete, String>;
/// Delete the given workspace scope; workspace path is the first argument.
fn delete_workspace(&self, workspace: &Path) -> Result<MemoryDelete, String>;
}

// ---------------------------------------------------------------------------
// Skill group (FEAT-022 D1)
// ---------------------------------------------------------------------------
Expand Down
71 changes: 67 additions & 4 deletions crates/command-contract/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,67 @@
//! `CommandHandler<crate::commands::CommandResult>`.

use crate::facets::{
CommandCostContext, CommandMediaContext, CommandModePolicyContext, CommandModelContext,
CommandPresentationContext, CommandProjectContext, CommandSessionContext,
CommandCostContext, CommandMediaContext, CommandMemoryContext, CommandModePolicyContext,
CommandModelContext, CommandPresentationContext, CommandProjectContext, CommandSessionContext,
CommandSkillGroupContext, CommandSkillsContext, CommandSystemPromptContext,
CommandWorkspaceContext,
};

/// Exact host capabilities exposed to one contextual command handler.
///
/// The set lives in the external contract crate so command registrations can
/// declare least authority without naming the TUI host. The dispatcher uses
/// the declaration to populate only those slots in [`CommandContexts`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CommandCapabilities(u16);

impl CommandCapabilities {
pub const NONE: Self = Self(0);
pub const SESSION: Self = Self(1 << 0);
pub const MODEL: Self = Self(1 << 1);
pub const COST: Self = Self(1 << 2);
pub const MODE_POLICY: Self = Self(1 << 3);
pub const SYSTEM_PROMPT: Self = Self(1 << 4);
pub const SKILLS: Self = Self(1 << 5);
pub const WORKSPACE: Self = Self(1 << 6);
pub const PRESENTATION: Self = Self(1 << 7);
pub const MEDIA: Self = Self(1 << 8);
/// Memory-group host data (FEAT-019 D1).
pub const MEMORY: Self = Self(1 << 9);
/// Project-group host data (FEAT-021 D1).
pub const PROJECT: Self = Self(1 << 10);
/// Skills-group host data (FEAT-022 D1).
pub const SKILL_GROUP: Self = Self(1 << 11);

pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}

pub const fn contains(self, capability: Self) -> bool {
!capability.is_empty() && self.0 & capability.0 == capability.0
}

pub const fn is_empty(self) -> bool {
self.0 == 0
}
}

impl std::ops::BitOr for CommandCapabilities {
type Output = Self;

fn bitor(self, rhs: Self) -> Self::Output {
self.union(rhs)
}
}

/// A command handler that is either argument-only or capability-scoped.
#[derive(Clone, Copy)]
pub enum CommandHandler<R> {
Pure(fn(Option<&str>) -> R),
Contextual(fn(CommandContexts<'_>, Option<&str>) -> R),
Contextual {
capabilities: CommandCapabilities,
handler: fn(CommandContexts<'_>, Option<&str>) -> R,
},
}

/// Transport envelope with one independently optional facet slot.
Expand All @@ -28,6 +79,7 @@ pub struct CommandContexts<'a> {
workspace: Option<&'a mut dyn CommandWorkspaceContext>,
presentation: Option<&'a mut dyn CommandPresentationContext>,
media: Option<&'a mut dyn CommandMediaContext>,
memory: Option<&'a mut dyn CommandMemoryContext>,
project: Option<&'a mut dyn CommandProjectContext>,
skill_group: Option<&'a mut dyn CommandSkillGroupContext>,
}
Expand All @@ -43,6 +95,7 @@ pub struct ContextParts<'a> {
pub workspace: Option<&'a mut dyn CommandWorkspaceContext>,
pub presentation: Option<&'a mut dyn CommandPresentationContext>,
pub media: Option<&'a mut dyn CommandMediaContext>,
pub memory: Option<&'a mut dyn CommandMemoryContext>,
pub project: Option<&'a mut dyn CommandProjectContext>,
pub skill_group: Option<&'a mut dyn CommandSkillGroupContext>,
}
Expand All @@ -59,6 +112,7 @@ impl<'a> CommandContexts<'a> {
workspace: None,
presentation: None,
media: None,
memory: None,
project: None,
skill_group: None,
}
Expand All @@ -75,6 +129,7 @@ impl<'a> CommandContexts<'a> {
workspace: self.workspace,
presentation: self.presentation,
media: self.media,
memory: self.memory,
project: self.project,
skill_group: self.skill_group,
}
Expand Down Expand Up @@ -149,6 +204,14 @@ impl<'a> CommandContexts<'a> {
self
}

pub fn with_memory(mut self, value: &'a mut dyn CommandMemoryContext) -> Self {
assert!(
self.memory.replace(value).is_none(),
"memory facet already set"
);
self
}

pub fn with_project(mut self, value: &'a mut dyn CommandProjectContext) -> Self {
assert!(
self.project.replace(value).is_none(),
Expand All @@ -160,7 +223,7 @@ impl<'a> CommandContexts<'a> {
pub fn with_skill_group(mut self, value: &'a mut dyn CommandSkillGroupContext) -> Self {
assert!(
self.skill_group.replace(value).is_none(),
"skill_group facet already set"
"skill-group facet already set"
);
self
}
Expand Down
2 changes: 1 addition & 1 deletion crates/command-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub mod metadata;
pub mod types;

pub use facets::*;
pub use handler::{CommandContexts, CommandHandler, ContextParts};
pub use handler::{CommandCapabilities, CommandContexts, CommandHandler, ContextParts};
pub use metadata::{CommandDiscovery, CommandInfo, RegisterCommand};
pub use types::*;

Expand Down
Loading
Loading