Skip to content

Commit 381ab6a

Browse files
feat: implement shared detector framework and evidence schema (issue #18)
* feat(rust): add shared client detector framework and evidence schema (issue #18) * test(docs): align frontend contracts and boundary docs with detector schema (issue #18)
1 parent a001791 commit 381ab6a

29 files changed

+619
-82
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Tauri 2 + React/TypeScript desktop foundation for managing MCP and Skills across
3232
- `src/backend/`: Typed frontend contracts and Tauri invoke client.
3333
- `src-tauri/`: Rust backend and Tauri application shell.
3434
- `src-tauri/src/domain/`: Domain layer (adapter interfaces, client profiles, capabilities).
35+
- `src-tauri/src/detection/`: Shared detector framework and per-client detectors.
3536
- `src-tauri/src/adapters/`: Client-specific adapter implementations.
3637
- `src-tauri/src/infra/`: Infrastructure composition (adapter registry).
3738
- `src-tauri/src/application/`: Use-case service layer consumed by commands.

docs/architecture/module-boundaries.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@ This project uses explicit backend layers to keep client-specific behavior isola
77
- `domain/`
88
- Owns adapter interface (`ClientAdapter`) and client profile/capability definitions.
99
- Contains types that describe adapter behavior without binding to concrete client implementations.
10+
- `detection/`
11+
- Owns detector interface (`ClientDetector`) and shared detection framework.
12+
- Defines detector registry and per-client detector implementations that emit one schema.
1013
- `adapters/`
1114
- Contains concrete adapter implementations for each supported client.
1215
- Current scaffold adapters: Claude Code, Codex CLI, Cursor, Codex App.
1316
- `infra/`
1417
- Provides infrastructure wiring (`AdapterRegistry`) that composes adapters.
1518
- Exposes registry lookup/iteration used by application services.
1619
- `application/`
17-
- Coordinates use-cases (`AdapterService`) and maps adapter outputs to command contracts.
20+
- Coordinates use-cases (`AdapterService`) and maps detector/adapter outputs to command contracts.
1821
- `commands/`
1922
- Thin Tauri command boundary; handles envelope metadata and delegates business flow.
2023

2124
## Extension Rule
2225

23-
To add a new client adapter, implement `ClientAdapter` in a new file under `adapters/` and register it in `infra/adapter_registry.rs`. Command and UI layers should remain unchanged.
26+
To add a new client adapter, implement `ClientAdapter` in a new file under `adapters/` and register it in `infra/adapter_registry.rs`.
27+
To add a new detector, implement `ClientDetector` in `detection/clients/` and register it in `detection/detector_registry.rs`.
28+
Command and UI layers should remain unchanged.

src-tauri/src/adapters/claude_code.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use crate::{
2-
contracts::{common::ResourceKind, detect::ClientDetection, mutate::MutationAction},
2+
contracts::{common::ResourceKind, mutate::MutationAction},
33
domain::{
44
AdapterListResult, AdapterMutationResult, CLAUDE_CODE_PROFILE, ClientAdapter, ClientProfile,
55
},
66
};
77

8-
use super::placeholder::{detect_placeholder, list_placeholder, mutate_placeholder};
8+
use super::placeholder::{list_placeholder, mutate_placeholder};
99

1010
pub struct ClaudeCodeAdapter;
1111

@@ -20,10 +20,6 @@ impl ClientAdapter for ClaudeCodeAdapter {
2020
&CLAUDE_CODE_PROFILE
2121
}
2222

23-
fn detect(&self, include_versions: bool) -> ClientDetection {
24-
detect_placeholder(self.profile(), include_versions)
25-
}
26-
2723
fn list_resources(&self, resource_kind: ResourceKind) -> AdapterListResult {
2824
list_placeholder(self.profile(), resource_kind)
2925
}

src-tauri/src/adapters/codex_app.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use crate::{
2-
contracts::{common::ResourceKind, detect::ClientDetection, mutate::MutationAction},
2+
contracts::{common::ResourceKind, mutate::MutationAction},
33
domain::{
44
AdapterListResult, AdapterMutationResult, CODEX_APP_PROFILE, ClientAdapter, ClientProfile,
55
},
66
};
77

8-
use super::placeholder::{detect_placeholder, list_placeholder, mutate_placeholder};
8+
use super::placeholder::{list_placeholder, mutate_placeholder};
99

1010
pub struct CodexAppAdapter;
1111

@@ -20,10 +20,6 @@ impl ClientAdapter for CodexAppAdapter {
2020
&CODEX_APP_PROFILE
2121
}
2222

23-
fn detect(&self, include_versions: bool) -> ClientDetection {
24-
detect_placeholder(self.profile(), include_versions)
25-
}
26-
2723
fn list_resources(&self, resource_kind: ResourceKind) -> AdapterListResult {
2824
list_placeholder(self.profile(), resource_kind)
2925
}

src-tauri/src/adapters/codex_cli.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use crate::{
2-
contracts::{common::ResourceKind, detect::ClientDetection, mutate::MutationAction},
2+
contracts::{common::ResourceKind, mutate::MutationAction},
33
domain::{
44
AdapterListResult, AdapterMutationResult, CODEX_CLI_PROFILE, ClientAdapter, ClientProfile,
55
},
66
};
77

8-
use super::placeholder::{detect_placeholder, list_placeholder, mutate_placeholder};
8+
use super::placeholder::{list_placeholder, mutate_placeholder};
99

1010
pub struct CodexCliAdapter;
1111

@@ -20,10 +20,6 @@ impl ClientAdapter for CodexCliAdapter {
2020
&CODEX_CLI_PROFILE
2121
}
2222

23-
fn detect(&self, include_versions: bool) -> ClientDetection {
24-
detect_placeholder(self.profile(), include_versions)
25-
}
26-
2723
fn list_resources(&self, resource_kind: ResourceKind) -> AdapterListResult {
2824
list_placeholder(self.profile(), resource_kind)
2925
}

src-tauri/src/adapters/cursor.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use crate::{
2-
contracts::{common::ResourceKind, detect::ClientDetection, mutate::MutationAction},
2+
contracts::{common::ResourceKind, mutate::MutationAction},
33
domain::{
44
AdapterListResult, AdapterMutationResult, CURSOR_PROFILE, ClientAdapter, ClientProfile,
55
},
66
};
77

8-
use super::placeholder::{detect_placeholder, list_placeholder, mutate_placeholder};
8+
use super::placeholder::{list_placeholder, mutate_placeholder};
99

1010
pub struct CursorAdapter;
1111

@@ -20,10 +20,6 @@ impl ClientAdapter for CursorAdapter {
2020
&CURSOR_PROFILE
2121
}
2222

23-
fn detect(&self, include_versions: bool) -> ClientDetection {
24-
detect_placeholder(self.profile(), include_versions)
25-
}
26-
2723
fn list_resources(&self, resource_kind: ResourceKind) -> AdapterListResult {
2824
list_placeholder(self.profile(), resource_kind)
2925
}

src-tauri/src/adapters/placeholder.rs

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,8 @@
11
use crate::{
2-
contracts::{
3-
common::ResourceKind,
4-
detect::{ClientDetection, DetectionEvidence, DetectionStatus},
5-
mutate::MutationAction,
6-
},
2+
contracts::{common::ResourceKind, mutate::MutationAction},
73
domain::{AdapterListResult, AdapterMutationResult, ClientProfile},
84
};
95

10-
pub fn detect_placeholder(
11-
profile: &'static ClientProfile,
12-
include_versions: bool,
13-
) -> ClientDetection {
14-
ClientDetection {
15-
client: profile.kind,
16-
status: DetectionStatus::Absent,
17-
evidence: DetectionEvidence {
18-
binary_path: None,
19-
config_path: None,
20-
version: include_versions.then_some("not_collected".to_string()),
21-
},
22-
note: format!(
23-
"{} adapter scaffold is ready. Detection implementation will be added in issue #19/#20.",
24-
profile.display_name
25-
),
26-
}
27-
}
28-
296
pub fn list_placeholder(
307
profile: &'static ClientProfile,
318
resource_kind: ResourceKind,

src-tauri/src/application/adapter_service.rs

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,31 @@ use crate::{
55
list::{ListResourcesRequest, ListResourcesResponse},
66
mutate::{MutateResourceRequest, MutateResourceResponse},
77
},
8+
detection::DetectorRegistry,
89
infra::AdapterRegistry,
910
};
1011

1112
pub struct AdapterService<'a> {
12-
registry: &'a AdapterRegistry,
13+
adapter_registry: &'a AdapterRegistry,
14+
detector_registry: &'a DetectorRegistry,
1315
}
1416

1517
impl<'a> AdapterService<'a> {
16-
pub fn new(registry: &'a AdapterRegistry) -> Self {
17-
Self { registry }
18+
pub fn new(
19+
adapter_registry: &'a AdapterRegistry,
20+
detector_registry: &'a DetectorRegistry,
21+
) -> Self {
22+
Self {
23+
adapter_registry,
24+
detector_registry,
25+
}
1826
}
1927

2028
pub fn detect_clients(&self, request: DetectClientsRequest) -> DetectClientsResponse {
2129
let clients = self
22-
.registry
30+
.detector_registry
2331
.all()
24-
.map(|adapter| adapter.detect(request.include_versions))
32+
.map(|detector| detector.detect(&request))
2533
.collect();
2634

2735
DetectClientsResponse { clients }
@@ -31,7 +39,7 @@ impl<'a> AdapterService<'a> {
3139
&self,
3240
request: ListResourcesRequest,
3341
) -> Result<ListResourcesResponse, CommandError> {
34-
let Some(adapter) = self.registry.find(request.client) else {
42+
let Some(adapter) = self.adapter_registry.find(request.client) else {
3543
return Err(CommandError::internal(format!(
3644
"No adapter registered for '{}'.",
3745
request.client.as_str()
@@ -60,7 +68,7 @@ impl<'a> AdapterService<'a> {
6068
));
6169
}
6270

63-
let Some(adapter) = self.registry.find(request.client) else {
71+
let Some(adapter) = self.adapter_registry.find(request.client) else {
6472
return Err(CommandError::internal(format!(
6573
"No adapter registered for '{}'.",
6674
request.client.as_str()
@@ -84,35 +92,42 @@ mod tests {
8492
use crate::{
8593
contracts::{
8694
common::{ClientKind, ResourceKind},
87-
detect::DetectClientsRequest,
95+
detect::{DetectClientsRequest, DetectionStatus},
8896
list::ListResourcesRequest,
8997
mutate::{MutateResourceRequest, MutationAction},
9098
},
99+
detection::DetectorRegistry,
91100
infra::AdapterRegistry,
92101
};
93102

94103
#[test]
95-
fn detect_clients_uses_every_registered_adapter() {
96-
let registry = AdapterRegistry::with_default_adapters();
97-
let service = AdapterService::new(&registry);
104+
fn detect_clients_uses_every_registered_detector() {
105+
let adapter_registry = AdapterRegistry::with_default_adapters();
106+
let detector_registry = DetectorRegistry::with_default_detectors();
107+
let service = AdapterService::new(&adapter_registry, &detector_registry);
98108

99109
let response = service.detect_clients(DetectClientsRequest {
100110
include_versions: true,
101111
});
102112

103113
assert_eq!(response.clients.len(), 4);
104-
assert!(
105-
response
106-
.clients
107-
.iter()
108-
.all(|entry| entry.evidence.version.is_some())
109-
);
114+
assert!(response.clients.iter().all(|entry| entry.confidence <= 100));
115+
assert!(response.clients.iter().all(|entry| {
116+
matches!(
117+
entry.status,
118+
DetectionStatus::Detected
119+
| DetectionStatus::Partial
120+
| DetectionStatus::Absent
121+
| DetectionStatus::Error
122+
)
123+
}));
110124
}
111125

112126
#[test]
113127
fn list_resources_routes_by_requested_client() {
114-
let registry = AdapterRegistry::with_default_adapters();
115-
let service = AdapterService::new(&registry);
128+
let adapter_registry = AdapterRegistry::with_default_adapters();
129+
let detector_registry = DetectorRegistry::with_default_detectors();
130+
let service = AdapterService::new(&adapter_registry, &detector_registry);
116131

117132
let response = service
118133
.list_resources(ListResourcesRequest {
@@ -133,8 +148,9 @@ mod tests {
133148

134149
#[test]
135150
fn mutate_resource_validates_target_id_before_adapter_call() {
136-
let registry = AdapterRegistry::with_default_adapters();
137-
let service = AdapterService::new(&registry);
151+
let adapter_registry = AdapterRegistry::with_default_adapters();
152+
let detector_registry = DetectorRegistry::with_default_detectors();
153+
let service = AdapterService::new(&adapter_registry, &detector_registry);
138154

139155
let error = service
140156
.mutate_resource(&MutateResourceRequest {

src-tauri/src/commands/detect.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub fn detect_clients(
2323
return CommandEnvelope::failure(CommandError::shutting_down(), meta);
2424
}
2525

26-
let service = AdapterService::new(state.adapter_registry());
26+
let service = AdapterService::new(state.adapter_registry(), state.detector_registry());
2727

2828
CommandEnvelope::success(service.detect_clients(request), meta)
2929
}

src-tauri/src/commands/list.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub fn list_resources(
2020
return CommandEnvelope::failure(CommandError::shutting_down(), meta);
2121
}
2222

23-
let service = AdapterService::new(state.adapter_registry());
23+
let service = AdapterService::new(state.adapter_registry(), state.detector_registry());
2424

2525
match service.list_resources(request) {
2626
Ok(response) => CommandEnvelope::success(response, meta),

0 commit comments

Comments
 (0)