From 906abe01f242e73dc42154430aa70829fb411a04 Mon Sep 17 00:00:00 2001 From: limityan Date: Tue, 19 May 2026 11:26:19 +0800 Subject: [PATCH 1/2] refactor(function-agents): wire commit facade path Route Git commit-message generation through the product-domain runtime facade with core-owned Git and AI adapters, keep Startchat work-state on the existing path, and add boundary checks for the migration seam. --- docs/architecture/core-decomposition.md | 8 +- docs/plans/core-decomposition-plan.md | 4 +- scripts/check-core-boundaries.mjs | 57 +++++++++++ src/crates/core/AGENTS.md | 4 + .../git-func-agent/commit_generator.rs | 98 +++---------------- .../core/src/function_agents/port_adapters.rs | 32 ++++-- src/crates/product-domains/AGENTS.md | 8 +- .../src/function_agents/ports.rs | 30 ++++-- .../tests/function_agent_contracts.rs | 71 ++++++++++---- 9 files changed, 180 insertions(+), 132 deletions(-) diff --git a/docs/architecture/core-decomposition.md b/docs/architecture/core-decomposition.md index 626ea31c4..c73507a13 100644 --- a/docs/architecture/core-decomposition.md +++ b/docs/architecture/core-decomposition.md @@ -257,9 +257,11 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate - 当前 `product-domains` runtime port/facade closure 只迁移 port-backed owner orchestration:MiniApp 的 deps/restart/recompile/sync/rollback 状态持久化可经 storage facade 执行,function-agent commit / work-state facade 可基于 Git/AI port - 组装结果。core 仍持有 MiniApp filesystem IO、compiler 调度、worker process、host - dispatch、built-in seed/update,以及 function-agent Git/AI service、prompt template、 - JSON extraction 和 error mapping;现有 function-agent 产品路径尚未切到新 facade。 + 组装结果。Git commit-message 产品路径可通过 core-owned Git/AI adapter 接入该 facade; + Startchat work-state 仍需先保留 git-state、git-diff fallback 与 time-info 等价性后再接线。 + core 仍持有 MiniApp filesystem IO、compiler 调度、worker process、host dispatch、 + built-in seed/update,以及 function-agent Git/AI service adapter、prompt template、 + JSON extraction 和 error mapping。 ## 产品表面边界(Product Surface Boundary) diff --git a/docs/plans/core-decomposition-plan.md b/docs/plans/core-decomposition-plan.md index 795df0fd1..5fe3ac3ce 100644 --- a/docs/plans/core-decomposition-plan.md +++ b/docs/plans/core-decomposition-plan.md @@ -1128,7 +1128,7 @@ product-full = ["miniapp", "function-agents"] - 2026-05-18 update: MiniApp draft manifest/response DTO, draft/customization storage path helpers, import layout / fallback payload contracts, manager lifecycle state-transition helpers, runtime executable search-plan helpers, customization draft-apply metadata policy, and built-in update/decline metadata decisions have been moved to `bitfun-product-domains::miniapp`; core continues to own draft/import filesystem IO, compile orchestration, built-in asset seeding/source-hash lookup, host dispatch execution, `PathManager` integration, worker process execution, and compatibility facades. The current PR also records core-owned MiniApp import / sync / recompile / rollback / dependency-state behavior as migration-before tests, including the existing `sync_from_fs` snapshot boundary. - 已迁移到 `bitfun-product-domains::function_agents`:公共 `common` 类型、git/startchat function-agent 的纯 DTO 类型、git function-agent 的纯路径 / 变更分类 / commit summary / message assembly / prompt format / commit type parser / AI response parsing policy、startchat prompt / action / AI response parsing policy / git porcelain / diff combine / time-of-day helper、Git/AI port contract,以及只读本地文件的 project context analyzer;core-owned Git snapshot adapter 已由等价测试覆盖,AI client、Git service、prompt template、AI request、JSON extraction、错误映射与分析运行逻辑仍留在 core。 - 2026-05-18 update: Git function-agent diff truncation and commit prompt preparation are now owner-crate helpers used by core; AI client calls, prompt template ownership, JSON extraction, error mapping, and runtime analysis execution remain core-owned. The current PR adds focused core snapshots for staged-only Git commit diff collection and AI response JSON extraction / error mapping before any Git/AI runtime migration. -- 2026-05-19 update: `bitfun-product-domains` now owns port-backed MiniApp runtime-state and function-agent runtime facades. Core delegates only MiniApp storage-backed lifecycle persistence through the MiniApp facade; compilation, source reads, storage IO adapter, worker process execution, host dispatch, built-in seeding, Git service calls, AI calls, prompt templates, JSON extraction, and concrete error mapping remain core-owned. The function-agent facade and core AI adapter shape are contract-ready but existing product call paths are not rewired yet. +- 2026-05-19 update: `bitfun-product-domains` now owns port-backed MiniApp runtime-state and function-agent runtime facades. Core delegates only MiniApp storage-backed lifecycle persistence through the MiniApp facade; compilation, source reads, storage IO adapter, worker process execution, host dispatch, built-in seeding, prompt templates, JSON extraction, and concrete error mapping remain core-owned. The Git commit-message product path now routes through the function-agent facade using core-owned Git/AI adapters; Startchat work-state remains on the existing core path until its git-state, git-diff fallback, and time-info behavior are equivalence-locked for wiring. - boundary check 已补充 product-domain owner anchor:`MiniAppStoragePort` / `MiniAppRuntimePort` 的 core adapter、MiniApp host/customization 纯 contract、MiniApp manager preflight tests、function-agent Git adapter 与 AI response parsing helper 必须存在,防止把 port contract 或 pure parser 误读成 storage IO、worker process、host dispatch、customization draft runtime、Git/AI service runtime 已完成迁移。 - miniapp runtime/storage/manager/host dispatch/exporter/builtin 与 function-agent 运行逻辑继续迁移前,需要先确认 agent/tool/provider port 和 Git/AI service 边界。 @@ -1741,7 +1741,7 @@ git diff -- package.json scripts/dev.cjs scripts/desktop-tauri-build.mjs scripts 15. 已完成:agent tools + `tool-packs` owner 化低风险闭环;tool contract / DTO、runtime restriction、path resolution、portable context facts/provider、generic registry / static provider installation / dynamic provider container 已归属 `bitfun-agent-tools`,`tool-packs` 只提供计划内 feature-group scaffold,core 保留 core-owned product provider groups、snapshot decorator、`ToolUseContext` 和 concrete tool implementation,后续外移需单独 service port/provider 设计。 16. 已完成:关键语义回归 baseline,不移动 runtime owner。覆盖 MCP config failure / catalog invalidation / 既有 list-changed helper / dynamic manifest、tool manifest / `GetToolSpec`、product-domains adapter equivalence、remote workspace search fallback 的 focused tests 或 snapshots。 17. 已完成:remote-connect runtime 当前批次收口。已基于当前 port baseline 记录 remote command/response、remote model catalog、poll response、model catalog delta、session restore、active turn、cancel、image context、tracker event、queue/event fanout 的输入输出和验证命令;tracker state / registry lifecycle、legacy image context fallback / preference、restore target decision、cancel decision 与 remote file transfer size/chunk/name policy 已迁入 `bitfun-services-integrations`。dispatcher / product execution、`ImageContextData` adapter、file IO/path resolution、terminal pre-warm 与 workspace/session restore 执行显式保留在 core-owned runtime;后续只有在另起 port/provider 设计且 focused regression 继续通过时才允许继续移动这些 runtime owner,不能把 generic attachment guard 当作已接入多模态行为。 -18. 当前 PR:`product-domains` runtime port/facade closure。已迁入 MiniApp storage-backed runtime-state facade 与 function-agent Git/AI port-backed runtime facade,并补充 focused contract tests;core 只对 MiniApp deps/restart/recompile/sync/rollback 的状态持久化委托 facade,仍保留 `PathManager` 注入、filesystem IO、worker process execution、host dispatch 执行、built-in asset seeding/source-hash lookup、Git/AI service 调用、prompt template、JSON extraction 和 error mapping adapter。function-agent facade 与 core AI adapter 只作为后续接线入口,现有产品路径暂不切换。 +18. 当前阶段:`product-domains` runtime port/facade closure。已迁入 MiniApp storage-backed runtime-state facade 与 function-agent Git/AI port-backed runtime facade,并补充 focused contract tests;core 只对 MiniApp deps/restart/recompile/sync/rollback 的状态持久化委托 facade,仍保留 `PathManager` 注入、filesystem IO、worker process execution、host dispatch 执行、built-in asset seeding/source-hash lookup、prompt template、JSON extraction 和 error mapping adapter。Git commit-message 产品路径已通过 core-owned Git/AI adapter 接入 function-agent facade;Startchat work-state 仍保留在旧 core 路径,后续必须先等价锁定 git-state、git-diff fallback 与 time-info 行为再接线。 19. 后续独立评估:`bitfun-core default = []`、per-product feature set、依赖版本收敛或构建收益优化;任何收益声明都需要记录 `cargo check -p bitfun-core`、workspace check 和目标 crate check 的前后数据。 冗余清理 PR 不进入上述主线序号。只有在满足 `0A.6` 的绝对等价要求时,才可以插入到相邻里程碑之间,并且不得与主线拆分 PR 混合。 diff --git a/scripts/check-core-boundaries.mjs b/scripts/check-core-boundaries.mjs index 92545e52a..53efdc7ed 100644 --- a/scripts/check-core-boundaries.mjs +++ b/scripts/check-core-boundaries.mjs @@ -499,6 +499,26 @@ const forbiddenContentRules = [ }, ], }, + { + path: 'src/crates/core/src/function_agents/git-func-agent/commit_generator.rs', + patterns: [ + { + regex: /\bGitService::get_status\b/, + message: + 'Git function-agent commit generator must use CoreFunctionAgentGitAdapter through FunctionAgentRuntimeFacade', + }, + { + regex: /\bAIAnalysisService::new_with_agent_config\b/, + message: + 'Git function-agent commit generator must use CoreFunctionAgentAiAdapter through FunctionAgentRuntimeFacade', + }, + { + regex: /\bto_string_lossy\b/, + message: + 'Git function-agent commit generator must preserve PathBuf paths when routing through the facade', + }, + ], + }, { path: 'src/crates/core/src/service/mcp/server/config.rs', patterns: [ @@ -2219,6 +2239,25 @@ const requiredContentRules = [ }, ], }, + { + path: 'src/crates/core/src/function_agents/git-func-agent/commit_generator.rs', + reason: + 'Git function-agent commit generation must route through the product-domain runtime facade while core keeps concrete adapters', + patterns: [ + { + regex: /\bFunctionAgentRuntimeFacade\b/, + message: 'missing product-domain function-agent runtime facade routing', + }, + { + regex: /\bCoreFunctionAgentGitAdapter\b/, + message: 'missing core-owned Git adapter wiring', + }, + { + regex: /\bCoreFunctionAgentAiAdapter\b/, + message: 'missing core-owned AI adapter wiring', + }, + ], + }, { path: 'src/crates/product-domains/src/function_agents/ports.rs', reason: @@ -2240,6 +2279,14 @@ const requiredContentRules = [ regex: /\bgit_work_state_from_snapshot\b/, message: 'missing Startchat Git snapshot projection helper', }, + { + regex: /\bStartchatTimeSnapshot\b/, + message: 'missing Startchat time snapshot contract', + }, + { + regex: /\bstartchat_time_snapshot\b/, + message: 'missing Startchat time snapshot port', + }, ], }, { @@ -3131,6 +3178,14 @@ function runManifestParserSelfTest() { 'parse_commit_response_preserves_core_json_extraction_and_error_mapping', ], }, + { + path: 'src/crates/core/src/function_agents/git-func-agent/commit_generator.rs', + contracts: [ + 'FunctionAgentRuntimeFacade', + 'CoreFunctionAgentGitAdapter', + 'CoreFunctionAgentAiAdapter', + ], + }, { path: 'src/crates/product-domains/src/function_agents/ports.rs', contracts: [ @@ -3138,6 +3193,8 @@ function runManifestParserSelfTest() { 'generate_commit_message', 'analyze_work_state', 'git_work_state_from_snapshot', + 'StartchatTimeSnapshot', + 'startchat_time_snapshot', ], }, { diff --git a/src/crates/core/AGENTS.md b/src/crates/core/AGENTS.md index dfac74afd..a70eb12e3 100644 --- a/src/crates/core/AGENTS.md +++ b/src/crates/core/AGENTS.md @@ -52,6 +52,10 @@ SessionManager → Session → DialogTurn → ModelRound - When touching session/token usage paths, keep `cached_content_token_count` as cache reads/hits and `cache_creation_token_count` as a separate provider fact. +- Function-agent commit-message orchestration may route through + `bitfun-product-domains`; keep Git/AI service adapters, prompt templates, + JSON extraction, and error mapping core-owned until a reviewed migration + proves equivalence. - Do not add new cross-layer references from `service` to `agentic` without a small port/interface boundary. - Do not move platform-specific logic, build-script behavior, or product diff --git a/src/crates/core/src/function_agents/git-func-agent/commit_generator.rs b/src/crates/core/src/function_agents/git-func-agent/commit_generator.rs index 85b435f2e..d280df27c 100644 --- a/src/crates/core/src/function_agents/git-func-agent/commit_generator.rs +++ b/src/crates/core/src/function_agents/git-func-agent/commit_generator.rs @@ -1,15 +1,16 @@ -use super::ai_service::AIAnalysisService; -use super::context_analyzer::ContextAnalyzer; use super::types::*; -use crate::function_agents::common::{AgentError, AgentResult}; +use crate::function_agents::common::AgentResult; +use crate::function_agents::port_adapters::{ + CoreFunctionAgentAiAdapter, CoreFunctionAgentGitAdapter, +}; use crate::infrastructure::ai::AIClientFactory; -use crate::service::git::{GitDiffParams, GitService}; +use bitfun_product_domains::function_agents::ports::FunctionAgentRuntimeFacade; /** * Git Function Agent - commit message generator * * Uses AI to deeply analyze code changes and generate compliant commit messages */ -use log::{debug, info}; +use log::info; use std::path::Path; use std::sync::Arc; @@ -26,88 +27,11 @@ impl CommitGenerator { repo_path ); - let status = GitService::get_status(repo_path) + let git_adapter = CoreFunctionAgentGitAdapter::default(); + let ai_adapter = CoreFunctionAgentAiAdapter::new(factory); + let facade = FunctionAgentRuntimeFacade::new(&git_adapter, &ai_adapter); + facade + .generate_commit_message(repo_path.to_path_buf(), options) .await - .map_err(|e| AgentError::git_error(format!("Failed to get Git status: {}", e)))?; - - let changed_files: Vec = status.staged.iter().map(|f| f.path.clone()).collect(); - - if changed_files.is_empty() { - return Err(AgentError::invalid_input( - "Staging area is empty, please stage files first", - )); - } - - debug!( - "Staged files: count={}, files={:?}", - changed_files.len(), - changed_files - ); - - let diff_content = Self::get_full_diff(repo_path).await?; - - if diff_content.trim().is_empty() { - return Err(AgentError::invalid_input("Diff content is empty")); - } - - let project_context = ContextAnalyzer::analyze_project_context(repo_path) - .await - .unwrap_or_default(); // Fallback to default on failure - - debug!( - "Project context: type={}, tech_stack={:?}", - project_context.project_type, project_context.tech_stack - ); - - let ai_service = - AIAnalysisService::new_with_agent_config(factory, "git-func-agent").await?; - - let ai_analysis = ai_service - .generate_commit_message_ai(&diff_content, &project_context, &options) - .await?; - - debug!( - "AI analysis completed: commit_type={:?}, confidence={}", - ai_analysis.commit_type, ai_analysis.confidence - ); - - let changes_summary = super::utils::build_changes_summary_from_paths( - &changed_files, - status.staged.len(), - status.unstaged.len(), - ); - - let full_message = super::utils::assemble_commit_message( - &ai_analysis.title, - &ai_analysis.body, - &ai_analysis.breaking_changes, - ); - - Ok(CommitMessage { - title: ai_analysis.title, - body: ai_analysis.body, - footer: ai_analysis.breaking_changes, - full_message, - commit_type: ai_analysis.commit_type, - scope: ai_analysis.scope, - confidence: ai_analysis.confidence, - changes_summary, - }) - } - - async fn get_full_diff(repo_path: &Path) -> AgentResult { - let diff_params = GitDiffParams { - staged: Some(true), - stat: Some(false), - files: None, - ..Default::default() - }; - - let diff = GitService::get_diff(repo_path, &diff_params) - .await - .map_err(|e| AgentError::git_error(format!("Failed to get diff: {}", e)))?; - - debug!("Got staged diff: length={} chars", diff.len()); - Ok(diff) } } diff --git a/src/crates/core/src/function_agents/port_adapters.rs b/src/crates/core/src/function_agents/port_adapters.rs index af39b09f8..45f533159 100644 --- a/src/crates/core/src/function_agents/port_adapters.rs +++ b/src/crates/core/src/function_agents/port_adapters.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use bitfun_product_domains::function_agents::ports::{ CommitAiAnalysisRequest, FunctionAgentAiPort, FunctionAgentFuture, FunctionAgentGitPort, - GitCommitSnapshot, StartchatGitSnapshot, WorkStateAiAnalysisRequest, + GitCommitSnapshot, StartchatGitSnapshot, StartchatTimeSnapshot, WorkStateAiAnalysisRequest, }; use bitfun_product_domains::function_agents::startchat_func_agent::AheadBehind; use bitfun_product_domains::function_agents::{ @@ -21,15 +21,29 @@ use crate::service::git::{GitDiffParams, GitService}; pub struct CoreFunctionAgentGitAdapter; impl FunctionAgentGitPort for CoreFunctionAgentGitAdapter { - fn git_commit_snapshot(&self, repo_path: String) -> FunctionAgentFuture<'_, GitCommitSnapshot> { - Box::pin(async move { Self::build_git_commit_snapshot(PathBuf::from(repo_path)).await }) + fn git_commit_snapshot( + &self, + repo_path: PathBuf, + ) -> FunctionAgentFuture<'_, GitCommitSnapshot> { + Box::pin(async move { Self::build_git_commit_snapshot(repo_path).await }) } fn startchat_git_snapshot( &self, - repo_path: String, + repo_path: PathBuf, ) -> FunctionAgentFuture<'_, StartchatGitSnapshot> { - Box::pin(async move { Self::build_startchat_git_snapshot(PathBuf::from(repo_path)).await }) + Box::pin(async move { Self::build_startchat_git_snapshot(repo_path).await }) + } + + fn startchat_time_snapshot( + &self, + repo_path: PathBuf, + ) -> FunctionAgentFuture<'_, StartchatTimeSnapshot> { + Box::pin(async move { + Ok(StartchatTimeSnapshot { + last_commit_timestamp: git_last_commit_timestamp(&repo_path), + }) + }) } } @@ -277,7 +291,7 @@ mod tests { let adapter = CoreFunctionAgentGitAdapter::default(); let snapshot = adapter - .git_commit_snapshot(repo.path().to_string_lossy().to_string()) + .git_commit_snapshot(repo.path().to_path_buf()) .await .unwrap(); @@ -303,7 +317,7 @@ mod tests { let adapter = CoreFunctionAgentGitAdapter::default(); let snapshot = adapter - .git_commit_snapshot(repo.path().to_string_lossy().to_string()) + .git_commit_snapshot(repo.path().to_path_buf()) .await .unwrap(); @@ -328,7 +342,7 @@ mod tests { let adapter = CoreFunctionAgentGitAdapter::default(); let snapshot = adapter - .startchat_git_snapshot(repo.path().to_string_lossy().to_string()) + .startchat_git_snapshot(repo.path().to_path_buf()) .await .unwrap(); @@ -348,7 +362,7 @@ mod tests { let adapter = CoreFunctionAgentGitAdapter::default(); let result = adapter - .startchat_git_snapshot(repo.path().to_string_lossy().to_string()) + .startchat_git_snapshot(repo.path().to_path_buf()) .await; assert!(result.is_err()); diff --git a/src/crates/product-domains/AGENTS.md b/src/crates/product-domains/AGENTS.md index d93cad993..2aeba0d5c 100644 --- a/src/crates/product-domains/AGENTS.md +++ b/src/crates/product-domains/AGENTS.md @@ -42,12 +42,12 @@ moves here gradually. - `function-agents` owns pure function-agent DTOs, prompt assembly helpers, commit prompt preparation, AI-response parsing policy, diff truncation policy, local file-shape analysis, Git/AI port traits, and port-backed runtime facade - orchestration. + orchestration, including the commit-message facade used by core adapters. - Core still owns MiniApp filesystem IO, worker process execution, host dispatch execution, built-in asset seeding/source-hash lookup, `PathManager` - integration, function-agent Git/AI calls, prompt templates, JSON extraction, - error mapping, and any product call-path rewiring not covered by equivalence - tests. + integration, function-agent Git/AI service adapters, prompt templates, JSON + extraction, error mapping, and Startchat work-state product path wiring until + equivalence tests cover that migration. ## Verification diff --git a/src/crates/product-domains/src/function_agents/ports.rs b/src/crates/product-domains/src/function_agents/ports.rs index 75e24be87..ca7e83c08 100644 --- a/src/crates/product-domains/src/function_agents/ports.rs +++ b/src/crates/product-domains/src/function_agents/ports.rs @@ -16,6 +16,7 @@ use crate::function_agents::startchat_func_agent::{ }; use serde::{Deserialize, Serialize}; use std::future::Future; +use std::path::PathBuf; use std::pin::Pin; pub type FunctionAgentFuture<'a, T> = Pin> + Send + 'a>>; @@ -50,6 +51,12 @@ pub struct StartchatGitSnapshot { pub last_commit_timestamp: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StartchatTimeSnapshot { + pub last_commit_timestamp: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WorkStateAiAnalysisRequest { @@ -59,11 +66,16 @@ pub struct WorkStateAiAnalysisRequest { } pub trait FunctionAgentGitPort: Send + Sync { - fn git_commit_snapshot(&self, repo_path: String) -> FunctionAgentFuture<'_, GitCommitSnapshot>; + fn git_commit_snapshot(&self, repo_path: PathBuf) + -> FunctionAgentFuture<'_, GitCommitSnapshot>; fn startchat_git_snapshot( &self, - repo_path: String, + repo_path: PathBuf, ) -> FunctionAgentFuture<'_, StartchatGitSnapshot>; + fn startchat_time_snapshot( + &self, + repo_path: PathBuf, + ) -> FunctionAgentFuture<'_, StartchatTimeSnapshot>; } /// Future AI boundary for function agents. @@ -100,7 +112,7 @@ impl<'a> FunctionAgentRuntimeFacade<'a> { pub async fn generate_commit_message( &self, - repo_path: String, + repo_path: PathBuf, options: CommitMessageOptions, ) -> AgentResult { let snapshot = self.git.git_commit_snapshot(repo_path).await?; @@ -147,12 +159,17 @@ impl<'a> FunctionAgentRuntimeFacade<'a> { pub async fn analyze_work_state( &self, - repo_path: String, + repo_path: PathBuf, options: WorkStateOptions, now_timestamp: i64, current_hour: u32, analyzed_at: String, ) -> AgentResult { + let time_snapshot = self + .git + .startchat_time_snapshot(repo_path.clone()) + .await + .ok(); let snapshot = if options.analyze_git { self.git.startchat_git_snapshot(repo_path).await.ok() } else { @@ -170,7 +187,8 @@ impl<'a> FunctionAgentRuntimeFacade<'a> { } else { String::new() }; - let time_info = time_info_from_snapshot(snapshot.as_ref(), now_timestamp, current_hour); + let time_info = + time_info_from_snapshot(time_snapshot.as_ref(), now_timestamp, current_hour); let ai_analysis = self .ai @@ -222,7 +240,7 @@ pub fn git_work_state_from_snapshot(snapshot: &StartchatGitSnapshot) -> GitWorkS } pub fn time_info_from_snapshot( - snapshot: Option<&StartchatGitSnapshot>, + snapshot: Option<&StartchatTimeSnapshot>, now_timestamp: i64, current_hour: u32, ) -> TimeInfo { diff --git a/src/crates/product-domains/tests/function_agent_contracts.rs b/src/crates/product-domains/tests/function_agent_contracts.rs index 57eae6807..146c02969 100644 --- a/src/crates/product-domains/tests/function_agent_contracts.rs +++ b/src/crates/product-domains/tests/function_agent_contracts.rs @@ -10,7 +10,7 @@ use bitfun_product_domains::function_agents::{ }, ports::{ CommitAiAnalysisRequest, FunctionAgentAiPort, FunctionAgentFuture, FunctionAgentGitPort, - FunctionAgentRuntimeFacade, GitCommitSnapshot, StartchatGitSnapshot, + FunctionAgentRuntimeFacade, GitCommitSnapshot, StartchatGitSnapshot, StartchatTimeSnapshot, WorkStateAiAnalysisRequest, }, startchat_func_agent::{ @@ -22,6 +22,7 @@ use bitfun_product_domains::function_agents::{ AgentErrorType, Language, }; use std::future::Future; +use std::path::PathBuf; use std::pin::pin; use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; @@ -30,7 +31,7 @@ struct FunctionAgentPortStub; impl FunctionAgentGitPort for FunctionAgentPortStub { fn git_commit_snapshot( &self, - _repo_path: String, + _repo_path: PathBuf, ) -> FunctionAgentFuture<'_, GitCommitSnapshot> { Box::pin(async { Ok(GitCommitSnapshot { @@ -45,7 +46,7 @@ impl FunctionAgentGitPort for FunctionAgentPortStub { fn startchat_git_snapshot( &self, - _repo_path: String, + _repo_path: PathBuf, ) -> FunctionAgentFuture<'_, StartchatGitSnapshot> { Box::pin(async { Ok(StartchatGitSnapshot { @@ -62,6 +63,17 @@ impl FunctionAgentGitPort for FunctionAgentPortStub { }) }) } + + fn startchat_time_snapshot( + &self, + _repo_path: PathBuf, + ) -> FunctionAgentFuture<'_, StartchatTimeSnapshot> { + Box::pin(async { + Ok(StartchatTimeSnapshot { + last_commit_timestamp: Some(900), + }) + }) + } } impl FunctionAgentAiPort for FunctionAgentPortStub { @@ -112,7 +124,7 @@ struct EmptyCommitPortStub; impl FunctionAgentGitPort for EmptyCommitPortStub { fn git_commit_snapshot( &self, - _repo_path: String, + _repo_path: PathBuf, ) -> FunctionAgentFuture<'_, GitCommitSnapshot> { Box::pin(async { Ok(GitCommitSnapshot { @@ -127,28 +139,46 @@ impl FunctionAgentGitPort for EmptyCommitPortStub { fn startchat_git_snapshot( &self, - _repo_path: String, + _repo_path: PathBuf, ) -> FunctionAgentFuture<'_, StartchatGitSnapshot> { FunctionAgentPortStub.startchat_git_snapshot(_repo_path) } + + fn startchat_time_snapshot( + &self, + _repo_path: PathBuf, + ) -> FunctionAgentFuture<'_, StartchatTimeSnapshot> { + FunctionAgentPortStub.startchat_time_snapshot(_repo_path) + } } -struct NoGitExpectedPortStub; +struct NoGitStateExpectedPortStub; -impl FunctionAgentGitPort for NoGitExpectedPortStub { +impl FunctionAgentGitPort for NoGitStateExpectedPortStub { fn git_commit_snapshot( &self, - _repo_path: String, + _repo_path: PathBuf, ) -> FunctionAgentFuture<'_, GitCommitSnapshot> { panic!("git_commit_snapshot should not be called") } fn startchat_git_snapshot( &self, - _repo_path: String, + _repo_path: PathBuf, ) -> FunctionAgentFuture<'_, StartchatGitSnapshot> { panic!("startchat_git_snapshot should not be called") } + + fn startchat_time_snapshot( + &self, + _repo_path: PathBuf, + ) -> FunctionAgentFuture<'_, StartchatTimeSnapshot> { + Box::pin(async { + Ok(StartchatTimeSnapshot { + last_commit_timestamp: Some(900), + }) + }) + } } fn block_on(future: F) -> F::Output { @@ -471,7 +501,7 @@ fn function_agent_ports_keep_ai_and_git_boundaries_explicit() { assert_eq!(json["language"], "English"); let port: &dyn FunctionAgentGitPort = &FunctionAgentPortStub; - let _future = port.git_commit_snapshot(".".to_string()); + let _future = port.git_commit_snapshot(PathBuf::from(".")); let ai_port: &dyn FunctionAgentAiPort = &FunctionAgentPortStub; let _future = ai_port.analyze_work_state(work_state_request); @@ -483,7 +513,7 @@ fn function_agent_runtime_facade_generates_commit_message_from_ports() { let facade = FunctionAgentRuntimeFacade::new(&ports, &ports); let message = block_on( - facade.generate_commit_message("repo".to_string(), CommitMessageOptions::default()), + facade.generate_commit_message(PathBuf::from("repo"), CommitMessageOptions::default()), ) .unwrap(); @@ -502,7 +532,7 @@ fn function_agent_runtime_facade_preserves_empty_staging_error() { let facade = FunctionAgentRuntimeFacade::new(&git, &ai); let error = block_on( - facade.generate_commit_message("repo".to_string(), CommitMessageOptions::default()), + facade.generate_commit_message(PathBuf::from("repo"), CommitMessageOptions::default()), ) .unwrap_err(); @@ -524,7 +554,7 @@ fn function_agent_runtime_facade_builds_work_state_from_ports_without_surface_lo }; let analysis = block_on(facade.analyze_work_state( - "repo".to_string(), + PathBuf::from("repo"), options, 960, 14, @@ -553,8 +583,8 @@ fn function_agent_runtime_facade_builds_work_state_from_ports_without_surface_lo } #[test] -fn function_agent_runtime_facade_honors_disabled_git_analysis_boundary() { - let git = NoGitExpectedPortStub; +fn function_agent_runtime_facade_honors_disabled_git_state_boundary_and_preserves_time_info() { + let git = NoGitStateExpectedPortStub; let ai = FunctionAgentPortStub; let facade = FunctionAgentRuntimeFacade::new(&git, &ai); let options = WorkStateOptions { @@ -565,7 +595,7 @@ fn function_agent_runtime_facade_honors_disabled_git_analysis_boundary() { }; let analysis = block_on(facade.analyze_work_state( - "repo".to_string(), + PathBuf::from("repo"), options, 960, 9, @@ -575,11 +605,10 @@ fn function_agent_runtime_facade_honors_disabled_git_analysis_boundary() { assert_eq!(analysis.current_state.summary, "stub"); assert!(analysis.current_state.git_state.is_none()); - assert!(analysis - .current_state - .time_info - .minutes_since_last_commit - .is_none()); + assert_eq!( + analysis.current_state.time_info.minutes_since_last_commit, + Some(1) + ); assert_eq!( analysis.current_state.time_info.time_of_day, TimeOfDay::Morning From fd821f848ce9412691d2966be6ed28437d1227c9 Mon Sep 17 00:00:00 2001 From: limityan Date: Tue, 19 May 2026 11:33:15 +0800 Subject: [PATCH 2/2] docs(function-agents): clarify facade wiring guardrail --- .../product-domains/src/function_agents/ports.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/crates/product-domains/src/function_agents/ports.rs b/src/crates/product-domains/src/function_agents/ports.rs index ca7e83c08..b067e9b26 100644 --- a/src/crates/product-domains/src/function_agents/ports.rs +++ b/src/crates/product-domains/src/function_agents/ports.rs @@ -80,9 +80,9 @@ pub trait FunctionAgentGitPort: Send + Sync { /// Future AI boundary for function agents. /// -/// This PR only defines the contract. Core still owns AI client selection, -/// prompt templates, response parsing, and error mapping; a concrete adapter -/// must add equivalence tests before any call site is wired through this trait. +/// Core still owns AI client selection, prompt templates, response parsing, and +/// error mapping. Product call sites may route through this trait only after +/// focused equivalence tests cover the specific facade path. pub trait FunctionAgentAiPort: Send + Sync { fn analyze_commit( &self, @@ -98,8 +98,9 @@ pub trait FunctionAgentAiPort: Send + Sync { /// /// It owns only pure orchestration over function-agent ports and DTO helpers. /// Core still owns Git/AI service calls, prompt templates, JSON extraction, -/// and concrete error mapping until the existing runtime path is explicitly -/// rewired with equivalence tests. +/// and concrete error mapping. Startchat product-path rewiring must remain +/// blocked until its Git state, diff fallback, and time-info behavior are +/// equivalence-locked. pub struct FunctionAgentRuntimeFacade<'a> { git: &'a dyn FunctionAgentGitPort, ai: &'a dyn FunctionAgentAiPort,