본문으로 건너뛰기

ADR-011: 독립 분석 파이프라인

항목
상태Accepted
날짜2026-03-18
범위신규 maekon-analysis crate, AnalysisProvider port, 스케줄러 통합, Suggestion 모델 통일

배경

클라이언트는 풍부한 데스크톱 활동 데이터(앱 전환, OCR 텍스트, 윈도우 제목, focus 메트릭)를 수집해 SQLite 에 저장합니다. 현재 제안은 규칙 기반(FocusAnalyzer) 또는 서버 의존(SSE) 둘 중 하나입니다. 연결된 서버 없이 수집된 컨텍스트를 LLM 에 입력해 실행 가능한 제안을 만드는 독립 분석 사이클이 필요합니다. 같은 로직은 추후 서버의 AI Intelligence 도메인으로 portable 해야 합니다.

본 ADR 은 기존 ADR 들이 다루지 않는 5가지 아키텍처 결정을 다룹니다:

  1. 신규 어댑터 crate 생성
  2. 분석 전용 port 계약
  3. 다중 port 소비를 위한 Orchestrator 패턴
  4. 스케줄러 루프 확장
  5. Suggestion 모델 통일

결정

§1 신규 어댑터 Crate: maekon-analysis

crates/ 하위에 새 워크스페이스 멤버 maekon-analysis 를 생성합니다.

의존성 규칙 (ADR-001 §6 확장):

maekon-core ← maekon-analysis (신규)
← maekon-monitor
← maekon-vision
← ...
maekon-analysis ← src-tauri (바이너리에서 소비)
  • maekon-core (port trait + 도메인 모델) 에만 의존해야 합니다.
  • 다른 어댑터 crate (maekon-network, maekon-storage 등) 에 의존해서는 안 됩니다.
  • src-tauri 가 DI 를 통해 구체 어댑터를 maekon-analysis 에 wire 합니다.
  • 에러 타입은 thiserror 사용 (라이브러리 crate, ADR-001 §1).
  • 테스트는 ADR-001 §5 를 따릅니다 — #[cfg(test)] 모듈 안에 수동 mock.

네이밍 컨벤션: maekon-{domain} — domain 은 crate 의 목적을 설명하는 단일 단어.

Crate 구조 (파일이 500 line 을 초과하면 ADR-003 적용):

crates/maekon-analysis/
├── Cargo.toml
├── src/
│ ├── lib.rs # pub re-export
│ ├── analyzer.rs # ContextAnalyzer orchestrator
│ ├── pattern_miner.rs # 순수 알고리즘 패턴 감지
│ ├── assembler.rs # 컨텍스트 어셈블리 + PII 필터링
│ └── prompts.rs # 시스템 프롬프트 템플릿

§2 AnalysisProvider Port 계약

maekon-core/src/ports/analysis_provider.rs 에 새 port trait AnalysisProvider 를 정의합니다.

#[async_trait]
pub trait AnalysisProvider: Send + Sync {
async fn analyze(
&self,
context_json: &str,
system_prompt: &str,
) -> Result<Vec<Suggestion>, CoreError>;

fn provider_name(&self) -> &str;
}

설계 근거:

  • 기존 LlmProvider trait (UI 자동화용 InterpretedAction 반환) 와 분리.
  • Vec<Suggestion> 을 직접 반환 — LLM 응답 파싱은 orchestrator 가 아니라 어댑터의 책임. 중간 타입(SuggestionCandidate)은 어댑터 내부에 private 으로 유지.
  • raw context_json + system_prompt 문자열 수용 — orchestrator 가 프롬프트 구성을 제어하고, 어댑터가 HTTP transport 처리.

에러 매핑: LLM 특화 실패(잘못된 응답, content filter, 토큰 한계)는 ADR-019 에 따라 CoreError::Analysis { code: ProviderCode::AnalysisFailed, message } (wire: provider.analysis_failed) 사용. ADR-019 이전 시그니처는 CoreError::Analysis(String) 였습니다.

구현: maekon-network/src/analysis_client.rs 에 위치하며, RemoteLlmProvider 와 동일한 HTTP 클라이언트 인프라 재사용. 같은 struct 가 LlmProviderAnalysisProvider 모두 구현 가능.

계약 테스트 (ADR-001 §5 따름):

#[cfg(test)]
mod tests {
struct MockAnalysisProvider { ... }

#[async_trait]
impl AnalysisProvider for MockAnalysisProvider {
async fn analyze(&self, context: &str, prompt: &str)
-> Result<Vec<Suggestion>, CoreError> { ... }
fn provider_name(&self) -> &str { "mock" }
}
}

§3 Orchestrator 패턴

ContextAnalyzer구체 structmaekon-analysis 에 위치하며 port trait 가 아닙니다. 분석 사이클을 오케스트레이션하기 위해 다중 port 를 소비합니다.

왜 port 가 아닌가?

  • Port 는 단일 책임의 I/O 경계를 표현합니다 (ADR-001 §7).
  • StorageService, PatternMiner, ContextAssembler, AnalysisProvider 를 내부에서 호출하는 orchestrator 는 다중 책임을 가집니다.
  • port 로 만들면 모든 소비자가 전체 오케스트레이션 표면을 mock 해야 합니다.

패턴:

pub struct ContextAnalyzer {
storage: Arc<dyn StorageService>,
analysis_provider: Arc<dyn AnalysisProvider>,
pattern_miner: PatternMiner, // owned, 순수 알고리즘
context_assembler: ContextAssembler, // owned, 순수 builder
config: AnalysisConfig,
last_analysis_at: Mutex<Option<DateTime<Utc>>>,
}
  • Port 의존성은 생성자 주입 (Arc<dyn T>, ADR-001 §3 따름).
  • 순수 알고리즘 컴포넌트(PatternMiner, ContextAssembler)는 직접 소유 — 외부 I/O 가 없어 port 추상화 불필요.
  • 내부 가변성(Mutex)은 throttle 상태 추적용에만 사용 (ADR-001 §2 따름).
  • src-tauri/src/agent_runtime_support.rs 에서 다른 DI wiring 과 함께 생성.

선례: maekon-automationAutomationController 가 동일 패턴을 따릅니다 — 다중 port 를 소비하는 구체 struct.

§4 스케줄러 루프 확장

스케줄러에 10번째 백그라운드 루프로 새 분석 루프 추가.

통합 지점: src-tauri/src/scheduler/loops.rs — 신규 spawn_analysis_loop() 메서드.

루프 구조 (spawn_focus_loop, spawn_sync_loop 의 기존 패턴 따름):

pub(super) fn spawn_analysis_loop(
&self,
config: AnalysisConfig,
mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
) -> tokio::task::JoinHandle<()> {
let analyzer = self.context_analyzer.clone();
let storage = self.storage.clone();

tokio::spawn(async move {
let mut interval = tokio::time::interval(
Duration::from_secs(config.interval_secs)
);

loop {
tokio::select! {
_ = interval.tick() => {
match analyzer.analyze().await {
Ok(suggestions) => { /* store + notify */ }
Err(e) => warn!("analysis failure: {e}"),
}
}
_ = shutdown_rx.changed() => {
info!("analysis loop ended");
break;
}
}
}
})
}

네이밍 컨벤션: spawn_{name}_loop() — 기존 패턴과 일치.

이벤트 구동 경로: spawn_monitor_loop 안에 FocusAnalyzer.on_app_switch_with_context() 와 함께 wire. 두 경로 모두 앱 전환 시 병렬 실행되며, 둘 다 Suggestion 출력.

중복 제거: FocusAnalyzer (규칙) 와 ContextAnalyzer (LLM) 모두 같은 이벤트에 대해 제안을 만들면, LLM 기반이 우선 (정보 밀도가 높음). 저장 전 스케줄러가 dedup.

§5 Suggestion 모델 통일

LocalSuggestion 폐기. 모든 제안은 maekon-core/src/models/suggestion.rs 의 통일된 Suggestion 모델 사용.

새 필드: source: SuggestionSource — 서버 SSE 역직렬화 backward 호환을 위해 #[serde(default)].

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SuggestionSource {
#[default]
RuleBased,
LlmLocal,
LlmServer,
}

마이그레이션: FocusAnalyzerLocalSuggestion 대신 Suggestion 출력. 기존 LocalSuggestion enum variant 들은 적절한 suggestion_typecontentSuggestion 에 매핑.

SQLite 스키마: V8 마이그레이션이 통일된 suggestions 테이블을 만들어 local_suggestions 를 교체.

공존 규칙: 서버가 활성 상태이며 SSE 로 제안을 반환하고 있을 때는 로컬 LLM 분석(LlmLocal) 억제. 규칙 기반(RuleBased)은 항상 실행.

결과

  • 워크스페이스가 10 → 11 crate 로 증가.
  • maekon-analysis 가 단독으로 완전 테스트 가능 (maekon-core trait 에만 의존).
  • PatternMinerContextAssembler 가 server-portable (클라이언트 특화 의존성 없음).
  • 스케줄러가 9 → 10 루프로 증가.
  • LocalSuggestion 제거. 모든 코드 경로가 Suggestion 사용.
  • maekon-networkAnalysisProvider 어댑터를 orchestrator 변경 없이 server-side DSPy 파이프라인으로 swap 가능.

참고

  • ADR-001 §1-7: 에러 타입, async trait, DI, crate 경계, port
  • ADR-003: 디렉토리 모듈 패턴 (파일이 500 line 을 초과할 때 적용)
  • ADR-009: 클라이언트 아키텍처 베이스라인 (런타임 구성, delivery 레이어)
  • 설계 spec: 내부 standalone analysis pipeline 설계 노트