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
12 changes: 7 additions & 5 deletions docs/architecture/core-decomposition.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,14 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate
- 中风险:在 owner crate 内为纯模块补 feature group、把 core 中的重依赖改为 optional 但
仍由 `product-full` 启用、把只依赖 port 的 helper 迁入 owner crate。
- 当前 `product-domains` 可继续承载 MiniApp runtime search plan、worker install 命令选择、
package.json storage-shape helper、lifecycle / revision helper、host routing string helper 等纯决策 /
解析逻辑;实际 runtime detection、worker pool、storage IO、PathManager、进程执行、
host dispatch 执行与 builtin asset seeding 仍留在 core product runtime。
package.json storage-shape helper、lifecycle / revision helper、host routing / allowlist helper、
customization metadata / permission diff 等纯决策 / 解析逻辑;实际 runtime detection、worker pool、
storage IO、PathManager、进程执行、host dispatch 执行、customization draft 存储 / 应用与 builtin
asset seeding 仍留在 core product runtime。
- `product-domains` 可以先定义 MiniApp runtime/storage 与 function-agent Git/AI 的 port
contract;core-owned adapter 只能在不改变执行路径的前提下委托现有 service,并先补等价
测试。IO/进程/AI/Git 执行 owner 迁移仍属于后续高风险步骤。
contract,并承载 function-agent 的纯 prompt / AI response parsing policy;core-owned adapter
只能在不改变执行路径的前提下委托现有 service,并先补等价测试。IO/进程/AI/Git 执行 owner
迁移仍属于后续高风险步骤。
- 高风险:`ToolUseContext`、product tool registry / manifest / exposure / `GetToolSpec` owner 化、
MCP concrete tool integration、remote-connect、remote SSH runtime、miniapp / function-agent runtime、
agent registry、`bitfun-core default = []`
Expand Down
8 changes: 4 additions & 4 deletions docs/plans/core-decomposition-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -1120,9 +1120,9 @@ product-full = ["miniapp", "function-agents"]
**当前安全迁移状态(2026-05-14):**

- 已迁移到 `bitfun-product-domains::miniapp`:`types`、`bridge_builder`、`permission_policy`,core 旧路径继续 re-export。
- 已迁移到 `bitfun-product-domains::miniapp`:纯 compiler、export DTO、runtime detection DTO、runtime search path plan、worker install result DTO、worker install 命令选择、package.json storage-shape helper,以及 runtime/storage port contract;core `miniapp::compiler::compile` 继续映射为原 `BitFunResult` API,runtime detection / exporter / worker pool / storage IO 执行逻辑仍留在 core,目前仅通过 core-owned storage/runtime adapter 和等价测试保护现有路径。
- 已迁移到 `bitfun-product-domains::function_agents`:公共 `common` 类型、git/startchat function-agent 的纯 DTO 类型、git function-agent 的纯路径 / 变更分类 / commit summary / message assembly / prompt format / commit type parser helper、startchat prompt / action / 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。
- boundary check 已补充 product-domain owner anchor:`MiniAppStoragePort` / `MiniAppRuntimePort` 的 core adapter、function-agent Git adapter 仍必须存在,防止把 port contract 误读成 storage IO、worker processGit service runtime 已完成迁移。
- 已迁移到 `bitfun-product-domains::miniapp`:纯 compiler、export DTO、runtime detection DTO、runtime search path plan、worker install result DTO、worker install 命令选择、package.json storage-shape helper、lifecycle / revision helper、host routing string / allowlist policy helper、customization metadata / permission diff,以及 runtime/storage port contract;core `miniapp::compiler::compile` 继续映射为原 `BitFunResult` API,runtime detection / exporter / host dispatch 执行 / customization draft 存储与应用 / worker pool / storage IO 执行逻辑仍留在 core,目前仅通过 core-owned storage/runtime adapter 和等价测试保护现有路径。
- 已迁移到 `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。
- boundary check 已补充 product-domain owner anchor:`MiniAppStoragePort` / `MiniAppRuntimePort` 的 core adapter、MiniApp host/customization 纯 contract、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 边界。

**验证:**
Expand Down Expand Up @@ -1536,7 +1536,7 @@ cargo check --workspace
- 未声明完成的 P2/后续剩余部分:remote-ssh runtime、remote-connect 等重 service 迁移、`ToolUseContext` 外移、tool exposure / manifest / `GetToolSpec` owner 化、concrete tool implementation 迁移、product registry / manifest / provider assembly、miniapp/function-agent 运行逻辑迁移。这些会触碰 `PathManager`、`ToolUseContext`、workspace service、snapshot wrapper、prompt-visible tool catalog、`AgentSubmissionPort` 或 AI service 边界,需要在继续前显式确认。
- 本次 rebase 后重新核对最新主干 Deep Review capacity/cost/queue、context profile、evidence ledger 与 session manifest 变更:当前 PR 已完成 Git feature group 的 owner crate 归属迁移,但未改动这些 Deep Review 行为路径;后续迁移必须补端口设计和等价测试后再推进。
- 本次 rebase 后重新核对最新主干 tool 变更:on-demand tool spec discovery 新增 collapsed/expanded manifest、`GetToolSpec`、context-aware schema/description 与 unlock state。这不要求回退当前 P2 已完成内容,但要求后续 tool/provider 迁移先补 manifest / catalog / unlock 等价保护,且不得和 PR5 product-domain runtime 收口混合。
- PR5 已先推进低风险 product-domain slice:MiniApp 纯 compiler、export/runtime/worker DTO、runtime search plan、worker install 命令选择、package.json storage-shape helper、lifecycle / revision helper、host routing string helper、runtime/storage port contract,以及 git/startchat function-agent 纯 utils / commit summary / message assembly / prompt format / action normalization / git porcelain / diff combine / time-of-day / Git/AI port contract / project context analyzer 已移入 `bitfun-product-domains`,core 保留原路径兼容 wrapper;已新增 core-owned Git snapshot、MiniApp storage/runtime port adapter 等价测试。PathManager、Git/AI service、prompt template、builtin asset seeding、host dispatch 执行、worker pool / storage IO 执行逻辑和任何 tool runtime 仍未迁移。
- PR5 已先推进低风险 product-domain slice:MiniApp 纯 compiler、export/runtime/worker DTO、runtime search plan、worker install 命令选择、package.json storage-shape helper、lifecycle / revision helper、host routing string / allowlist policy helper、customization metadata / permission diff、runtime/storage port contract,以及 git/startchat function-agent 纯 utils / commit summary / message assembly / prompt format / AI response parsing policy / action normalization / git porcelain / diff combine / time-of-day / Git/AI port contract / project context analyzer 已移入 `bitfun-product-domains`,core 保留原路径兼容 wrapper;core 只保留 AI client 调用、JSON 提取、错误映射、Git service adapter 和原路径 facade。已新增 core-owned Git snapshot、MiniApp storage/runtime port adapter 等价测试。PathManager、Git/AI service、prompt template、builtin asset seeding、host dispatch 执行、customization draft 存储 / 应用、worker pool / storage IO 执行逻辑和任何 tool runtime 仍未迁移。
- 本次 P2 后续复核结论:上述高耦合剩余项不是纯文件搬迁;若继续迁移会改变依赖方向或需要新增 port/provider 行为合约。因此当前 PR 将它们显式保留为 core-owned runtime,只完成低风险 owner container 化,并通过 boundary check 防止已拆 owner crate 回流依赖 core。

**后续风险重排(2026-05-13):**
Expand Down
88 changes: 88 additions & 0 deletions scripts/check-core-boundaries.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,70 @@ const requiredContentRules = [
},
],
},
{
path: 'src/crates/product-domains/src/miniapp/host_routing.rs',
reason:
'product-domains owns MiniApp host-routing and allowlist string policy while core keeps host execution',
patterns: [
{
regex: /\bpub fn command_basename_for_allowlist\b/,
message: 'missing MiniApp command basename allowlist helper',
},
{
regex: /\bpub fn command_basename_allowed\b/,
message: 'missing MiniApp command allowlist policy helper',
},
{
regex: /\bpub fn host_allowed_by_allowlist\b/,
message: 'missing MiniApp host allowlist policy helper',
},
],
},
{
path: 'src/crates/product-domains/src/miniapp/customization.rs',
reason:
'product-domains owns MiniApp customization metadata and permission-diff contracts while core keeps draft storage/runtime',
patterns: [
{
regex: /\bpub struct MiniAppCustomizationMetadata\b/,
message: 'missing MiniApp customization metadata contract',
},
{
regex: /\bpub struct MiniAppPermissionDiff\b/,
message: 'missing MiniApp permission diff contract',
},
{
regex: /\bpub fn diff_permissions\b/,
message: 'missing MiniApp permission diff helper',
},
],
},
{
path: 'src/crates/product-domains/src/function_agents/startchat_func_agent/utils.rs',
reason:
'product-domains owns pure Startchat function-agent parsing policy while core keeps AI calls and error mapping',
patterns: [
{
regex: /\bpub struct ParsedCompleteAnalysis\b/,
message: 'missing Startchat complete-analysis parse result contract',
},
{
regex: /\bpub fn parse_complete_analysis_value\b/,
message: 'missing Startchat complete-analysis value parser',
},
],
},
{
path: 'src/crates/product-domains/src/function_agents/git_func_agent/utils.rs',
reason:
'product-domains owns pure Git function-agent response parsing policy while core keeps AI calls and error mapping',
patterns: [
{
regex: /\bpub fn parse_commit_analysis_value\b/,
message: 'missing Git function-agent commit analysis value parser',
},
],
},
{
path: 'src/crates/core/src/miniapp/js_worker_pool.rs',
reason:
Expand Down Expand Up @@ -2044,6 +2108,30 @@ function runManifestParserSelfTest() {
path: 'src/crates/product-domains/src/miniapp/storage.rs',
contracts: ['MiniAppStorageLayout', 'META_JSON', 'source_file_path', 'versions_dir'],
},
{
path: 'src/crates/product-domains/src/miniapp/host_routing.rs',
contracts: [
'command_basename_for_allowlist',
'command_basename_allowed',
'host_allowed_by_allowlist',
],
},
{
path: 'src/crates/product-domains/src/miniapp/customization.rs',
contracts: [
'MiniAppCustomizationMetadata',
'MiniAppPermissionDiff',
'diff_permissions',
],
},
{
path: 'src/crates/product-domains/src/function_agents/startchat_func_agent/utils.rs',
contracts: ['ParsedCompleteAnalysis', 'parse_complete_analysis_value'],
},
{
path: 'src/crates/product-domains/src/function_agents/git_func_agent/utils.rs',
contracts: ['parse_commit_analysis_value'],
},
];
for (const { path, contracts } of requiredContentContracts) {
const rule = requiredContentRules.find((rule) => rule.path === path);
Expand Down
21 changes: 2 additions & 19 deletions src/crates/core/src/function_agents/git-func-agent/ai_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use crate::util::types::Message;
* Handles AI client interaction and provides intelligent analysis for commit message generation
*/
use log::{debug, error, warn};
use serde_json::Value;
use std::sync::Arc;

/// Prompt template constants (embedded at compile time)
Expand Down Expand Up @@ -102,27 +101,11 @@ impl AIAnalysisService {
let json_str = crate::util::extract_json_from_ai_response(response)
.ok_or_else(|| AgentError::analysis_error("Cannot extract JSON from response"))?;

let value: Value = serde_json::from_str(&json_str).map_err(|e| {
let value: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
AgentError::analysis_error(format!("Failed to parse AI response: {}", e))
})?;

Ok(AICommitAnalysis {
commit_type: super::utils::parse_commit_type_label(
value["type"].as_str().unwrap_or("chore"),
),
scope: value["scope"].as_str().map(|s| s.to_string()),
title: value["title"]
.as_str()
.ok_or_else(|| AgentError::analysis_error("Missing title field"))?
.to_string(),
body: value["body"].as_str().map(|s| s.to_string()),
breaking_changes: value["breaking_changes"].as_str().map(|s| s.to_string()),
reasoning: value["reasoning"]
.as_str()
.unwrap_or("AI analysis")
.to_string(),
confidence: value["confidence"].as_f64().unwrap_or(0.8) as f32,
})
super::utils::parse_commit_analysis_value(&value).map_err(AgentError::analysis_error)
}

fn truncate_diff_if_needed(&self, diff: &str, max_chars: usize) -> String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,67 +115,39 @@ impl AIWorkStateService {
AgentError::internal_error(format!("Failed to parse complete analysis response: {}", e))
})?;

let summary = parsed["summary"]
.as_str()
.unwrap_or("You were working on development, with multiple files modified.")
.to_string();
let parsed_analysis = super::utils::parse_complete_analysis_value(&parsed);

let ongoing_work = Vec::new();

let predicted_actions = if let Some(actions_array) = parsed["predicted_actions"].as_array()
{
super::utils::parse_predicted_actions_from_values(actions_array)
} else {
Vec::new()
};
let predicted_actions_count = predicted_actions.len();
let predicted_actions = super::utils::normalize_predicted_actions(predicted_actions);

if predicted_actions_count < 3 {
if parsed_analysis.predicted_actions_count < 3 {
warn!(
"AI generated insufficient predicted actions ({}), adding defaults",
predicted_actions_count
parsed_analysis.predicted_actions_count
);
} else if predicted_actions_count > 3 {
} else if parsed_analysis.predicted_actions_count > 3 {
warn!(
"AI generated too many predicted actions ({}), truncating to 3",
predicted_actions_count
parsed_analysis.predicted_actions_count
);
}

let quick_actions = if let Some(actions_array) = parsed["quick_actions"].as_array() {
super::utils::parse_quick_actions_from_values(actions_array)
} else {
Vec::new()
};

let quick_actions_count = quick_actions.len();
let quick_actions = super::utils::limit_quick_actions(quick_actions);

if quick_actions_count < 6 {
if parsed_analysis.quick_actions_count < 6 {
// Don't fill defaults here, frontend has its own defaultActions with i18n support
warn!(
"AI generated insufficient quick actions ({}), frontend will use defaults",
quick_actions_count
parsed_analysis.quick_actions_count
);
} else if quick_actions_count > 6 {
} else if parsed_analysis.quick_actions_count > 6 {
warn!(
"AI generated too many quick actions ({}), truncating to 6",
quick_actions_count
parsed_analysis.quick_actions_count
);
}

debug!(
"Parsing completed: predicted_actions={}, quick_actions={}",
predicted_actions.len(),
quick_actions.len()
parsed_analysis.analysis.predicted_actions.len(),
parsed_analysis.analysis.quick_actions.len()
);

Ok(AIGeneratedAnalysis {
summary,
ongoing_work,
predicted_actions,
quick_actions,
})
Ok(parsed_analysis.analysis)
}
}
13 changes: 5 additions & 8 deletions src/crates/core/src/miniapp/host_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ use crate::miniapp::permission_policy::resolve_policy;
use crate::miniapp::types::MiniAppPermissions;
use crate::util::errors::{BitFunError, BitFunResult};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use bitfun_product_domains::miniapp::host_routing::command_basename_for_allowlist;
pub use bitfun_product_domains::miniapp::host_routing::is_host_primitive;
use bitfun_product_domains::miniapp::host_routing::{
command_basename_allowed, command_basename_for_allowlist, host_allowed_by_allowlist,
};
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
use std::time::Duration;
Expand Down Expand Up @@ -364,7 +366,7 @@ async fn dispatch_shell(
None => command.split_whitespace().next().unwrap_or(""),
};
let base = command_basename_for_allowlist(first_token);
if !allow.is_empty() && !allow.iter().any(|a| a.to_lowercase() == base) {
if !command_basename_allowed(&allow, &base) {
return Err(deny(format!("Command not in allowlist: {}", base)));
}

Expand Down Expand Up @@ -485,12 +487,7 @@ async fn dispatch_net(policy: &Value, name: &str, params: &Value) -> BitFunResul
.collect()
})
.unwrap_or_default();
if !allow.is_empty()
&& !allow.iter().any(|a| a == "*")
&& !allow
.iter()
.any(|a| host == *a || host.ends_with(&format!(".{}", a)))
{
if !host_allowed_by_allowlist(&allow, &host) {
return Err(deny(format!("Domain not in allowlist: {}", host)));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,21 @@ pub fn parse_commit_type_label(label: &str) -> CommitType {
_ => CommitType::Chore,
}
}

pub fn parse_commit_analysis_value(value: &serde_json::Value) -> Result<AICommitAnalysis, String> {
Ok(AICommitAnalysis {
commit_type: parse_commit_type_label(value["type"].as_str().unwrap_or("chore")),
scope: value["scope"].as_str().map(|s| s.to_string()),
title: value["title"]
.as_str()
.ok_or_else(|| "Missing title field".to_string())?
.to_string(),
body: value["body"].as_str().map(|s| s.to_string()),
breaking_changes: value["breaking_changes"].as_str().map(|s| s.to_string()),
reasoning: value["reasoning"]
.as_str()
.unwrap_or("AI analysis")
.to_string(),
confidence: value["confidence"].as_f64().unwrap_or(0.8) as f32,
})
}
Loading
Loading