diff --git a/docs/architecture/core-decomposition.md b/docs/architecture/core-decomposition.md index 03621004e..abc225c3d 100644 --- a/docs/architecture/core-decomposition.md +++ b/docs/architecture/core-decomposition.md @@ -189,7 +189,7 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate - 当前 `product-domains` 可继续承载 MiniApp runtime search plan、worker install 命令选择、 package.json storage-shape helper、lifecycle / revision helper、host routing / allowlist helper、 customization metadata / permission diff、built-in MiniApp bundle/hash/marker/source payload - seed-decision contract 等纯决策 / 解析逻辑;实际 runtime detection、worker pool、 + seed-decision contract、seed plan / marker wire helper 等纯决策 / 解析逻辑;实际 runtime detection、worker pool、 storage IO、PathManager、进程执行、host dispatch 执行、customization draft 存储 / 应用与 builtin asset include / seeding / marker IO / recompile 仍留在 core product runtime。最新内置 PR Review MiniApp 依赖 core asset include、user-data seed、customized update runtime 与 source-hash input @@ -210,7 +210,8 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate built-in asset seeding/source-hash input lookup, host-dispatch execution, `PathManager` integration, function-agent Git/AI calls, prompt templates, JSON extraction, and error mapping. The built-in MiniApp bundle/hash/marker/source payload - seed-decision contract can live in `bitfun-product-domains`, but bundled + seed-decision contract plus seed plan / marker wire helper can live in + `bitfun-product-domains`, but bundled asset includes, marker IO, customized update runtime, and recompile orchestration remain core-owned. - 高风险:`ToolUseContext`、product tool registry / runtime manifest assembly / `GetToolSpec` 执行 owner 化、 @@ -269,7 +270,8 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate `analyze_git=false` time-info 与 `analyzed_at` 时序。 core 仍持有 MiniApp filesystem IO、compiler 调度、worker process、host dispatch、 built-in asset include / seed / marker IO / recompile,以及 function-agent Git/AI service adapter、prompt template、 - JSON extraction 和 error mapping。 + JSON extraction 和 error mapping;`product-domains` 仅承接 extracted JSON string + 到 domain DTO 的解析 helper,不承接 extraction 或错误映射 owner。 ## 产品表面边界(Product Surface Boundary) diff --git a/docs/plans/core-decomposition-plan.md b/docs/plans/core-decomposition-plan.md index 093c5a549..b83b6e96b 100644 --- a/docs/plans/core-decomposition-plan.md +++ b/docs/plans/core-decomposition-plan.md @@ -1131,7 +1131,9 @@ product-full = ["miniapp", "function-agents"] - 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 asset include / seed / marker IO / recompile, prompt templates, JSON extraction, and concrete error mapping remain core-owned. The Git commit-message and Startchat work-state product paths now route through the function-agent facade using core-owned Git/AI adapters; Startchat wiring is guarded by focused tests for legacy git-state, no-HEAD git-diff fallback, and `analyze_git=false` time-info, while core keeps the previous post-analysis `analyzed_at` assignment. - 2026-05-19 built-in MiniApp contract update: built-in bundle shape, install marker DTO, content-hash helper, source/placeholder/package payload helpers, and seed-decision policy now live in `bitfun-product-domains::miniapp::builtin`; core still owns the bundled asset includes, user-data filesystem IO, marker read/write, customization metadata IO, source-hash input lookup, and recompile orchestration. -- boundary check 已补充 product-domain owner anchor:`MiniAppStoragePort` / `MiniAppRuntimePort` 的 core adapter、MiniApp host/customization/builtin 纯 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、builtin asset seeding runtime、Git/AI service runtime 已完成迁移。 +- 2026-05-19 follow-up: MiniApp built-in seed artifact / action resolution and install-marker serialize/parse helpers have also moved to `bitfun-product-domains::miniapp::builtin`; core still owns bundled asset includes, user-data filesystem reads/writes, marker read/write calls, local customization metadata IO, source-hash input lookup, timestamp source, and recompile orchestration. +- 2026-05-19 function-agent parser update: Git commit-message and Startchat complete-analysis JSON-string parsers now live in `bitfun-product-domains::function_agents`; core still owns AI service calls, prompt templates, `extract_json_from_ai_response`, log/error mapping, and runtime analysis orchestration. +- boundary check 已补充 product-domain owner anchor:`MiniAppStoragePort` / `MiniAppRuntimePort` 的 core adapter、MiniApp host/customization/builtin 纯 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、builtin asset seeding runtime、Git/AI service runtime、JSON extraction 或错误映射已完成迁移。 - miniapp runtime/storage/manager/host dispatch/exporter/builtin 与 function-agent 运行逻辑继续迁移前,需要先确认 agent/tool/provider port 和 Git/AI service 边界。 **验证:** @@ -1545,7 +1547,7 @@ cargo check --workspace - 未声明完成的 P2/后续剩余部分:remote-ssh runtime、remote-connect 等重 service 迁移、`ToolUseContext` 外移、runtime manifest assembly / `GetToolSpec` 执行 owner 化、concrete tool implementation 迁移、product registry / 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、import layout / fallback payload contract、lifecycle / revision helper、manager 纯状态转换 helper、host routing string / allowlist policy helper、customization metadata / permission diff、built-in bundle/hash/marker/source payload seed-decision contract、runtime/storage port contract,以及 git/startchat function-agent 纯 utils / commit summary / message assembly / prompt format / commit prompt preparation / 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 等价测试,并补齐 MiniApp manager import/sync/recompile/rollback/deps state、built-in asset seeding decision 等价测试与 function-agent staged diff / AI response error mapping 的迁移前快照。PathManager、Git/AI service、prompt template、builtin asset includes / seed / marker IO / recompile、host dispatch 执行、customization draft 存储 / 应用、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、import layout / fallback payload contract、lifecycle / revision helper、manager 纯状态转换 helper、host routing string / allowlist policy helper、customization metadata / permission diff、built-in bundle/hash/marker/source payload seed-decision contract、built-in seed plan / marker wire helper、runtime/storage port contract,以及 git/startchat function-agent 纯 utils / commit summary / message assembly / prompt format / commit prompt preparation / AI response parsing policy / JSON-string parsing helper / 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 等价测试,并补齐 MiniApp manager import/sync/recompile/rollback/deps state、built-in asset seeding decision 等价测试与 function-agent staged diff / AI response error mapping 的迁移前快照。PathManager、Git/AI service、prompt template、builtin asset includes / seed / marker IO / recompile、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):** @@ -1743,7 +1745,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. 当前阶段:`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 与 Startchat work-state 产品路径已通过 core-owned Git/AI adapter 接入 function-agent facade;Startchat 接线已用 no-HEAD diff fallback、非 Git 目录空状态和 `analyze_git=false` time-info 保护旧行为,`analyzed_at` 仍由 core 在 AI 分析完成后赋值。 +18. 当前阶段:`product-domains` runtime port/facade closure。已迁入 MiniApp storage-backed runtime-state facade、built-in seed plan / marker wire helper 与 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 与 Startchat work-state 产品路径已通过 core-owned Git/AI adapter 接入 function-agent facade;extracted JSON string 可委托 product-domain parser,但 JSON 提取、日志与错误映射仍在 core。Startchat 接线已用 no-HEAD diff fallback、非 Git 目录空状态和 `analyze_git=false` time-info 保护旧行为,`analyzed_at` 仍由 core 在 AI 分析完成后赋值。 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 a247305b9..c2969477e 100644 --- a/scripts/check-core-boundaries.mjs +++ b/scripts/check-core-boundaries.mjs @@ -1919,6 +1919,14 @@ const requiredContentRules = [ regex: /\bshould_seed_builtin_app\b/, message: 'missing product-domain built-in MiniApp seed decision use', }, + { + regex: /\bresolve_builtin_seed_check\b/, + message: 'missing product-domain built-in MiniApp seed check use', + }, + { + regex: /\bresolve_builtin_seed_action\b/, + message: 'missing product-domain built-in MiniApp seed action use', + }, { regex: /\bbuiltin_source_files\b/, message: 'missing product-domain built-in MiniApp source payload use', @@ -1931,10 +1939,18 @@ const requiredContentRules = [ regex: /\bread_builtin_install_marker\b/, message: 'missing core-owned built-in MiniApp marker read IO', }, + { + regex: /\bparse_builtin_install_marker\b/, + message: 'missing product-domain built-in MiniApp marker parse helper use', + }, { regex: /\bwrite_builtin_install_marker\b/, message: 'missing core-owned built-in MiniApp marker write IO', }, + { + regex: /\bserialize_builtin_install_marker\b/, + message: 'missing product-domain built-in MiniApp marker serialization helper use', + }, { regex: /\brecompile\b/, message: 'missing core-owned built-in MiniApp recompile orchestration', @@ -2288,6 +2304,10 @@ const requiredContentRules = [ regex: /\bextract_json_from_ai_response\b/, message: 'missing core-owned AI response JSON extraction', }, + { + regex: /\bparse_commit_analysis_json\b/, + message: 'missing product-domain Git function-agent JSON parser use', + }, { regex: /\bAgentError::analysis_error\b/, message: 'missing core-owned AI response error mapping', @@ -2298,6 +2318,37 @@ const requiredContentRules = [ }, ], }, + { + path: 'src/crates/core/src/function_agents/startchat-func-agent/ai_service.rs', + reason: + 'core must continue owning Startchat prompt template, AI call, JSON extraction, and error mapping until AI runtime migration is reviewed', + patterns: [ + { + regex: /\bconst WORK_STATE_ANALYSIS_PROMPT\b/, + message: 'missing core-owned Startchat prompt template', + }, + { + regex: /\bai_client\s*\.\s*send_message\b/, + message: 'missing core-owned Startchat AI call', + }, + { + regex: /\bextract_json_from_ai_response\b/, + message: 'missing core-owned Startchat JSON extraction', + }, + { + regex: /\bparse_complete_analysis_json\b/, + message: 'missing product-domain Startchat JSON parser use', + }, + { + regex: /\bAgentError::internal_error\b/, + message: 'missing core-owned Startchat error mapping', + }, + { + regex: /\bparse_complete_analysis_preserves_core_json_extraction_and_error_mapping\b/, + message: 'missing Startchat AI response boundary regression test', + }, + ], + }, { path: 'src/crates/core/src/function_agents/git-func-agent/commit_generator.rs', reason: @@ -2342,6 +2393,34 @@ const requiredContentRules = [ regex: /\bpub fn should_seed_builtin_app\b/, message: 'missing built-in MiniApp seed decision helper', }, + { + regex: /\bpub struct BuiltinSeedArtifacts\b/, + message: 'missing built-in MiniApp seed artifacts contract', + }, + { + regex: /\bpub enum BuiltinSeedCheck\b/, + message: 'missing built-in MiniApp seed check contract', + }, + { + regex: /\bpub enum BuiltinSeedAction\b/, + message: 'missing built-in MiniApp seed action contract', + }, + { + regex: /\bpub fn resolve_builtin_seed_check\b/, + message: 'missing built-in MiniApp seed check helper', + }, + { + regex: /\bpub fn resolve_builtin_seed_action\b/, + message: 'missing built-in MiniApp seed action helper', + }, + { + regex: /\bpub fn serialize_builtin_install_marker\b/, + message: 'missing built-in MiniApp marker serialization helper', + }, + { + regex: /\bpub fn parse_builtin_install_marker\b/, + message: 'missing built-in MiniApp marker parse helper', + }, { regex: /\bpub fn builtin_source_files\b/, message: 'missing built-in MiniApp source payload helper', @@ -2419,6 +2498,10 @@ const requiredContentRules = [ regex: /\bpub fn parse_complete_analysis_value\b/, message: 'missing Startchat complete-analysis value parser', }, + { + regex: /\bpub fn parse_complete_analysis_json\b/, + message: 'missing Startchat complete-analysis JSON parser', + }, ], }, { @@ -2430,6 +2513,10 @@ const requiredContentRules = [ regex: /\bpub fn parse_commit_analysis_value\b/, message: 'missing Git function-agent commit analysis value parser', }, + { + regex: /\bpub fn parse_commit_analysis_json\b/, + message: 'missing Git function-agent commit analysis JSON parser', + }, { regex: /\bpub fn truncate_diff_for_commit_prompt\b/, message: 'missing Git function-agent diff truncation helper', @@ -3177,10 +3264,14 @@ function runManifestParserSelfTest() { 'BUILTIN_APPS', 'builtin_content_hash', 'should_seed_builtin_app', + 'resolve_builtin_seed_check', + 'resolve_builtin_seed_action', 'builtin_source_files', 'BUILTIN_PLACEHOLDER_COMPILED_HTML', 'read_builtin_install_marker', + 'parse_builtin_install_marker', 'write_builtin_install_marker', + 'serialize_builtin_install_marker', 'recompile', 'load_customization_metadata', 'available_builtin_update', @@ -3194,6 +3285,13 @@ function runManifestParserSelfTest() { 'BUILTIN_INSTALL_MARKER', 'builtin_content_hash', 'should_seed_builtin_app', + 'BuiltinSeedArtifacts', + 'BuiltinSeedCheck', + 'BuiltinSeedAction', + 'resolve_builtin_seed_check', + 'resolve_builtin_seed_action', + 'serialize_builtin_install_marker', + 'parse_builtin_install_marker', 'builtin_source_files', 'BUILTIN_PLACEHOLDER_COMPILED_HTML', 'build_builtin_package_json', @@ -3323,10 +3421,22 @@ function runManifestParserSelfTest() { 'prepare_commit_prompt', 'send_message', 'extract_json_from_ai_response', + 'parse_commit_analysis_json', 'AgentError::analysis_error', 'parse_commit_response_preserves_core_json_extraction_and_error_mapping', ], }, + { + path: 'src/crates/core/src/function_agents/startchat-func-agent/ai_service.rs', + contracts: [ + 'WORK_STATE_ANALYSIS_PROMPT', + 'send_message', + 'extract_json_from_ai_response', + 'parse_complete_analysis_json', + 'AgentError::internal_error', + 'parse_complete_analysis_preserves_core_json_extraction_and_error_mapping', + ], + }, { path: 'src/crates/core/src/function_agents/git-func-agent/commit_generator.rs', contracts: [ @@ -3348,11 +3458,16 @@ function runManifestParserSelfTest() { }, { path: 'src/crates/product-domains/src/function_agents/startchat_func_agent/utils.rs', - contracts: ['ParsedCompleteAnalysis', 'parse_complete_analysis_value'], + contracts: ['ParsedCompleteAnalysis', 'parse_complete_analysis_value', 'parse_complete_analysis_json'], }, { path: 'src/crates/product-domains/src/function_agents/git_func_agent/utils.rs', - contracts: ['parse_commit_analysis_value', 'truncate_diff_for_commit_prompt', 'prepare_commit_prompt'], + contracts: [ + 'parse_commit_analysis_value', + 'parse_commit_analysis_json', + 'truncate_diff_for_commit_prompt', + 'prepare_commit_prompt', + ], }, { path: 'src/crates/core/src/miniapp/runtime_detect.rs', diff --git a/src/crates/core/AGENTS-CN.md b/src/crates/core/AGENTS-CN.md index e54ac7a7f..cbde21600 100644 --- a/src/crates/core/AGENTS-CN.md +++ b/src/crates/core/AGENTS-CN.md @@ -34,6 +34,14 @@ SessionManager → Session → DialogTurn → ModelRound - Tool 迁移必须保持 expanded/collapsed exposure、prompt 可见 manifest、`ToolUseContext.unlocked_collapsed_tools`,以及 desktop/MCP/ACP tool catalog 行为等价。 - 不要把 OpenAI Responses / Codex ChatGPT flat tool schema 等 provider-specific 序列化行为写进 core tool contract;AI adapter 负责 provider 序列化,core 保持 provider-neutral manifest。 - 调整 session/token usage 路径时,`cached_content_token_count` 必须继续表示 cache reads/hits,`cache_creation_token_count` 必须作为独立 provider fact 保留。 +- Function-agent commit-message 与 Startchat work-state orchestration 可以经由 + `bitfun-product-domains`;Git/AI service adapter、prompt template、JSON + extraction 和 error mapping 仍由 core 拥有。JSON extraction 之后的 JSON string + parser 可以放在 `bitfun-product-domains`。 +- MiniApp built-in bundle/hash/marker seed plan 与 marker wire helper 可以放在 + `bitfun-product-domains`;bundled asset include、filesystem writes、marker IO、 + customization metadata IO、recompile orchestration、worker process runtime 和 + host dispatch execution 仍由 core 拥有,直到有评审过的迁移和等价测试。 - 不要在没有小型 port/interface 边界的情况下新增 `service` 到 `agentic` 的跨层引用。 - 不要在 core 拆解中把平台专属逻辑、构建脚本行为或产品能力选择下沉到 shared core。 diff --git a/src/crates/core/AGENTS.md b/src/crates/core/AGENTS.md index 620148fc9..9a2a39611 100644 --- a/src/crates/core/AGENTS.md +++ b/src/crates/core/AGENTS.md @@ -55,11 +55,11 @@ SessionManager → Session → DialogTurn → ModelRound fact. - Function-agent commit-message and Startchat work-state 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. -- MiniApp built-in bundle/hash/marker seed-decision contracts may live in - `bitfun-product-domains`; keep bundled asset includes, filesystem writes, - marker IO, customization metadata IO, recompile orchestration, worker + templates, JSON extraction, and error mapping core-owned. JSON-string parsing + helpers may live in `bitfun-product-domains` after extraction has happened. +- MiniApp built-in bundle/hash/marker seed-plan and marker wire helpers may + live in `bitfun-product-domains`; keep bundled asset includes, filesystem + writes, marker IO, customization metadata IO, recompile orchestration, worker process runtime, and host dispatch execution core-owned until a reviewed migration proves equivalence. - Do not add new cross-layer references from `service` to `agentic` without a diff --git a/src/crates/core/src/function_agents/git-func-agent/ai_service.rs b/src/crates/core/src/function_agents/git-func-agent/ai_service.rs index 3e801af11..45e044641 100644 --- a/src/crates/core/src/function_agents/git-func-agent/ai_service.rs +++ b/src/crates/core/src/function_agents/git-func-agent/ai_service.rs @@ -7,7 +7,9 @@ use crate::util::types::Message; * * Handles AI client interaction and provides intelligent analysis for commit message generation */ -use bitfun_product_domains::function_agents::git_func_agent::prepare_commit_prompt; +use bitfun_product_domains::function_agents::git_func_agent::{ + parse_commit_analysis_json, prepare_commit_prompt, +}; use log::{debug, error, warn}; use std::sync::Arc; @@ -99,11 +101,7 @@ 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: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| { - AgentError::analysis_error(format!("Failed to parse AI response: {}", e)) - })?; - - super::utils::parse_commit_analysis_value(&value).map_err(AgentError::analysis_error) + parse_commit_analysis_json(&json_str).map_err(AgentError::analysis_error) } } diff --git a/src/crates/core/src/function_agents/startchat-func-agent/ai_service.rs b/src/crates/core/src/function_agents/startchat-func-agent/ai_service.rs index 096caa65a..e57828f21 100644 --- a/src/crates/core/src/function_agents/startchat-func-agent/ai_service.rs +++ b/src/crates/core/src/function_agents/startchat-func-agent/ai_service.rs @@ -2,6 +2,7 @@ use super::types::*; use crate::function_agents::common::{AgentError, AgentResult, Language}; use crate::infrastructure::ai::AIClient; use crate::util::types::Message; +use bitfun_product_domains::function_agents::startchat_func_agent::parse_complete_analysis_json; /** * AI analysis service * @@ -107,16 +108,11 @@ impl AIWorkStateService { debug!("Parsing JSON response: length={}", json_str.len()); - let parsed: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| { - error!( - "Failed to parse complete analysis response: {}, response: {}", - e, response - ); - AgentError::internal_error(format!("Failed to parse complete analysis response: {}", e)) + let parsed_analysis = parse_complete_analysis_json(&json_str).map_err(|message| { + error!("{}, response: {}", message, response); + AgentError::internal_error(message) })?; - let parsed_analysis = super::utils::parse_complete_analysis_value(&parsed); - if parsed_analysis.predicted_actions_count < 3 { warn!( "AI generated insufficient predicted actions ({}), adding defaults", @@ -151,3 +147,83 @@ impl AIWorkStateService { Ok(parsed_analysis.analysis) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::function_agents::common::AgentErrorType; + use crate::util::types::AIConfig; + use bitfun_ai_adapters::types::ReasoningMode; + + fn test_service() -> AIWorkStateService { + AIWorkStateService { + ai_client: Arc::new(AIClient::new(AIConfig { + name: "test".to_string(), + base_url: "http://127.0.0.1".to_string(), + request_url: "http://127.0.0.1".to_string(), + api_key: "test".to_string(), + model: "test-model".to_string(), + format: "openai".to_string(), + context_window: 8192, + max_tokens: None, + temperature: None, + top_p: None, + reasoning_mode: ReasoningMode::Default, + inline_think_in_text: false, + custom_headers: None, + custom_headers_mode: None, + skip_ssl_verify: false, + reasoning_effort: None, + thinking_budget_tokens: None, + custom_request_body: None, + custom_request_body_mode: None, + })), + } + } + + #[test] + fn parse_complete_analysis_preserves_core_json_extraction_and_error_mapping() { + let service = test_service(); + let analysis = service + .parse_complete_analysis( + r#"The answer is: +```json +{ + "summary": "Working on product-domain owner closure.", + "predicted_actions": [ + {"description": "Run checks", "priority": "High", "icon": "check", "is_reminder": false} + ], + "quick_actions": [ + {"title": "Status", "command": "git status", "icon": "git", "action_type": "ViewStatus"} + ] +} +``` +"#, + ) + .unwrap(); + + assert_eq!(analysis.summary, "Working on product-domain owner closure."); + assert_eq!(analysis.predicted_actions.len(), 3); + assert_eq!(analysis.quick_actions.len(), 1); + + let missing_json = service.parse_complete_analysis("no json here").unwrap_err(); + assert_eq!(missing_json.error_type, AgentErrorType::InternalError); + assert_eq!( + missing_json.message, + "Failed to extract JSON from analysis response" + ); + + let invalid_json = service + .parse_complete_analysis( + r#"```json +not json +```"#, + ) + .unwrap_err(); + assert_eq!(invalid_json.error_type, AgentErrorType::InternalError); + assert_eq!( + invalid_json.message, + "Failed to extract JSON from analysis response" + ); + } +} diff --git a/src/crates/core/src/miniapp/builtin/mod.rs b/src/crates/core/src/miniapp/builtin/mod.rs index e4ca41946..b640204b8 100644 --- a/src/crates/core/src/miniapp/builtin/mod.rs +++ b/src/crates/core/src/miniapp/builtin/mod.rs @@ -9,9 +9,10 @@ use crate::miniapp::manager::MiniAppManager; use crate::miniapp::types::MiniAppMeta; use crate::util::errors::{BitFunError, BitFunResult}; use bitfun_product_domains::miniapp::builtin::{ - build_builtin_package_json, builtin_content_hash, builtin_source_files, - should_seed_builtin_app, BuiltinInstallMarker, BuiltinMiniAppBundle, BUILTIN_INSTALL_MARKER, - BUILTIN_PLACEHOLDER_COMPILED_HTML, LEGACY_BUILTIN_VERSION_MARKER, + build_builtin_package_json, builtin_source_files, parse_builtin_install_marker, + resolve_builtin_seed_action, resolve_builtin_seed_check, serialize_builtin_install_marker, + BuiltinInstallMarker, BuiltinMiniAppBundle, BuiltinSeedAction, BuiltinSeedCheck, + BUILTIN_INSTALL_MARKER, BUILTIN_PLACEHOLDER_COMPILED_HTML, LEGACY_BUILTIN_VERSION_MARKER, }; use chrono::Utc; use std::path::Path; @@ -90,28 +91,35 @@ pub async fn seed_builtin_miniapps(manager: &Arc) -> BitFunResul async fn seed_one(manager: &Arc, app: &BuiltinApp) -> BitFunResult<()> { let app_dir = manager.path_manager().miniapp_dir(app.id); let marker_path = app_dir.join(BUILTIN_INSTALL_MARKER); - let content_hash = builtin_content_hash(app); + let installed_marker = read_builtin_install_marker(&marker_path).await?; + let seed_artifacts = match resolve_builtin_seed_check(app, installed_marker.as_ref()) { + BuiltinSeedCheck::Skip => return Ok(()), + BuiltinSeedCheck::NeedsSeed(artifacts) => artifacts, + }; - if let Some(marker) = read_builtin_install_marker(&marker_path).await? { - if !should_seed_builtin_app(app, &content_hash, Some(&marker)) { - return Ok(()); + let now = Utc::now().timestamp_millis(); + let has_local_override = match manager.load_customization_metadata(app.id).await { + Ok(Some(metadata)) => metadata.local_override, + Ok(None) => false, + Err(e) => { + log::warn!( + "read customization metadata for builtin miniapp '{}' failed: {}", + app.id, + e + ); + false } - } + }; - let now = Utc::now().timestamp_millis(); - match manager.load_customization_metadata(app.id).await { - Ok(Some(metadata)) if metadata.local_override => { + match resolve_builtin_seed_action(seed_artifacts, has_local_override) { + BuiltinSeedAction::PreserveLocalOverride(artifacts) => { let recorded = manager - .mark_builtin_update_available(app.id, app.version, &content_hash, now) + .mark_builtin_update_available(app.id, app.version, &artifacts.content_hash, now) .await?; - let marker = BuiltinInstallMarker { - version: app.version, - hash: content_hash, - }; - write_builtin_install_marker(&marker_path, &marker).await?; + write_builtin_install_marker(&marker_path, &artifacts.marker).await?; write_file( app_dir.join(LEGACY_BUILTIN_VERSION_MARKER), - &app.version.to_string(), + &artifacts.legacy_version, ) .await?; if recorded { @@ -129,16 +137,19 @@ async fn seed_one(manager: &Arc, app: &BuiltinApp) -> BitFunResu } return Ok(()); } - Ok(_) => {} - Err(e) => { - log::warn!( - "read customization metadata for builtin miniapp '{}' failed: {}", - app.id, - e - ); + BuiltinSeedAction::SeedBundle(artifacts) => { + seed_builtin_bundle(manager, app, artifacts, now).await } } +} +async fn seed_builtin_bundle( + manager: &Arc, + app: &BuiltinApp, + artifacts: bitfun_product_domains::miniapp::builtin::BuiltinSeedArtifacts, + now: i64, +) -> BitFunResult<()> { + let app_dir = manager.path_manager().miniapp_dir(app.id); let source_dir = app_dir.join("source"); tokio::fs::create_dir_all(&source_dir) .await @@ -191,21 +202,18 @@ async fn seed_one(manager: &Arc, app: &BuiltinApp) -> BitFunResu // Recompile to assemble the final compiled.html with bridge + theme + import map. manager.recompile(app.id, "dark", None).await?; - let marker = BuiltinInstallMarker { - version: app.version, - hash: content_hash, - }; - write_builtin_install_marker(&marker_path, &marker).await?; + let marker_path = app_dir.join(BUILTIN_INSTALL_MARKER); + write_builtin_install_marker(&marker_path, &artifacts.marker).await?; write_file( app_dir.join(LEGACY_BUILTIN_VERSION_MARKER), - &app.version.to_string(), + &artifacts.legacy_version, ) .await?; log::info!( "seeded builtin miniapp '{}' (v{}, {})", app.id, app.version, - marker.hash + artifacts.marker.hash ); Ok(()) } @@ -223,7 +231,7 @@ async fn read_builtin_install_marker(path: &Path) -> BitFunResult(&content) { + match parse_builtin_install_marker(&content) { Ok(marker) => Ok(Some(marker)), Err(error) => { log::warn!( @@ -240,7 +248,7 @@ async fn write_builtin_install_marker( path: &Path, marker: &BuiltinInstallMarker, ) -> BitFunResult<()> { - let content = serde_json::to_string_pretty(marker).map_err(BitFunError::from)?; + let content = serialize_builtin_install_marker(marker).map_err(BitFunError::from)?; write_file(path, &content).await } @@ -253,6 +261,7 @@ async fn write_file>(path: P, content: &str) -> BitFun #[cfg(test)] mod tests { use super::*; + use bitfun_product_domains::miniapp::builtin::{builtin_content_hash, should_seed_builtin_app}; use bitfun_product_domains::miniapp::customization::{ MiniAppCustomizationMetadata, MiniAppCustomizationOrigin, MiniAppCustomizationOriginKind, }; diff --git a/src/crates/product-domains/AGENTS-CN.md b/src/crates/product-domains/AGENTS-CN.md index cfd91ca1c..75d24987a 100644 --- a/src/crates/product-domains/AGENTS-CN.md +++ b/src/crates/product-domains/AGENTS-CN.md @@ -33,15 +33,18 @@ - `miniapp` 拥有 MiniApp DTO、compiler/bridge helper、storage/draft/import 文件形态、fallback payload、runtime search plan、worker install 命令选择、 lifecycle/revision 与 manager state-transition helper、host-routing string - policy、customization metadata policy、port trait,以及 storage-backed runtime + policy、customization metadata policy、built-in update/decline 决策、 + built-in bundle/hash/marker seed plan 与 marker wire helper、built-in + source/placeholder payload contract、port trait,以及 storage-backed runtime state facade。 - `function-agents` 拥有纯 DTO、prompt assembly、commit prompt preparation、 - AI response parsing policy、diff truncation policy、本地文件形态分析、 - Git/AI port trait,以及 port-backed runtime facade orchestration。 + AI response parsing policy、diff truncation policy、JSON string 到领域 DTO 的 + 解析 helper、本地文件形态分析、Git/AI port trait,以及 port-backed runtime + facade orchestration。 - Core 仍拥有 MiniApp filesystem IO、worker process、host dispatch、built-in - asset seeding/source-hash lookup、`PathManager` 集成、function-agent Git/AI - 调用、prompt template、JSON extraction、error mapping,以及尚未被等价测试覆盖的 - 产品调用路径切换。 + asset include/seeding、marker IO、recompile orchestration、source-hash lookup、 + `PathManager` 集成、function-agent Git/AI service adapter、prompt template、 + JSON extraction 和 error mapping。 ## 验证 diff --git a/src/crates/product-domains/AGENTS.md b/src/crates/product-domains/AGENTS.md index 593795cd0..7c114f3fe 100644 --- a/src/crates/product-domains/AGENTS.md +++ b/src/crates/product-domains/AGENTS.md @@ -37,14 +37,14 @@ moves here gradually. file shapes, import fallback payloads, runtime search-plan helpers, worker install command selection, lifecycle/revision and manager state-transition helpers, host-routing string policy, customization metadata policy including - built-in update/decline decisions, built-in bundle/hash/marker seed-decision - contracts, built-in source/placeholder payload contracts, port traits, and - storage-backed runtime state facade logic. + built-in update/decline decisions, built-in bundle/hash/marker seed plan and + marker wire helpers, built-in source/placeholder payload contracts, port + traits, and storage-backed runtime state facade logic. - `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, including the commit-message and Startchat work-state facades - used by core adapters. + JSON-string-to-domain parsing helpers, local file-shape analysis, Git/AI port + traits, and port-backed runtime facade orchestration, including the + commit-message and Startchat work-state facades used by core adapters. - Core still owns MiniApp filesystem IO, worker process execution, host dispatch execution, built-in asset includes/seeding, marker IO, recompile orchestration, source-hash input lookup, `PathManager` integration, function-agent Git/AI diff --git a/src/crates/product-domains/src/function_agents/git_func_agent/utils.rs b/src/crates/product-domains/src/function_agents/git_func_agent/utils.rs index 8d8241b43..37e44252a 100644 --- a/src/crates/product-domains/src/function_agents/git_func_agent/utils.rs +++ b/src/crates/product-domains/src/function_agents/git_func_agent/utils.rs @@ -294,3 +294,9 @@ pub fn parse_commit_analysis_value(value: &serde_json::Value) -> Result Result { + let value = serde_json::from_str::(json) + .map_err(|error| format!("Failed to parse AI response: {}", error))?; + parse_commit_analysis_value(&value) +} diff --git a/src/crates/product-domains/src/function_agents/startchat_func_agent/utils.rs b/src/crates/product-domains/src/function_agents/startchat_func_agent/utils.rs index fb56fee5f..ea54d72e8 100644 --- a/src/crates/product-domains/src/function_agents/startchat_func_agent/utils.rs +++ b/src/crates/product-domains/src/function_agents/startchat_func_agent/utils.rs @@ -137,6 +137,7 @@ pub fn limit_quick_actions(mut actions: Vec) -> Vec { actions } +#[derive(Debug, Clone)] pub struct ParsedCompleteAnalysis { pub analysis: AIGeneratedAnalysis, pub predicted_actions_count: usize, @@ -175,6 +176,12 @@ pub fn parse_complete_analysis_value(parsed: &serde_json::Value) -> ParsedComple } } +pub fn parse_complete_analysis_json(json: &str) -> Result { + let parsed = serde_json::from_str::(json) + .map_err(|error| format!("Failed to parse complete analysis response: {}", error))?; + Ok(parse_complete_analysis_value(&parsed)) +} + pub fn parse_action_priority_label(label: &str) -> ActionPriority { match label { "High" => ActionPriority::High, diff --git a/src/crates/product-domains/src/miniapp/builtin.rs b/src/crates/product-domains/src/miniapp/builtin.rs index 3b8ac7ccc..6c20b6b0e 100644 --- a/src/crates/product-domains/src/miniapp/builtin.rs +++ b/src/crates/product-domains/src/miniapp/builtin.rs @@ -17,6 +17,25 @@ pub struct BuiltinInstallMarker { pub hash: String, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BuiltinSeedArtifacts { + pub content_hash: String, + pub marker: BuiltinInstallMarker, + pub legacy_version: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BuiltinSeedCheck { + Skip, + NeedsSeed(BuiltinSeedArtifacts), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BuiltinSeedAction { + PreserveLocalOverride(BuiltinSeedArtifacts), + SeedBundle(BuiltinSeedArtifacts), +} + /// Pure built-in MiniApp asset bundle shape. The owning runtime still decides /// how assets are embedded, seeded, compiled, and persisted. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -46,6 +65,62 @@ pub fn builtin_content_hash(app: &BuiltinMiniAppBundle) -> String { format!("sha256:{}", hex_encode(&hasher.finalize())) } +pub fn build_builtin_install_marker( + app: &BuiltinMiniAppBundle, + content_hash: &str, +) -> BuiltinInstallMarker { + BuiltinInstallMarker { + version: app.version, + hash: content_hash.to_string(), + } +} + +pub fn legacy_builtin_version_marker_content(app: &BuiltinMiniAppBundle) -> String { + app.version.to_string() +} + +pub fn build_builtin_seed_artifacts(app: &BuiltinMiniAppBundle) -> BuiltinSeedArtifacts { + let content_hash = builtin_content_hash(app); + BuiltinSeedArtifacts { + marker: build_builtin_install_marker(app, &content_hash), + legacy_version: legacy_builtin_version_marker_content(app), + content_hash, + } +} + +pub fn resolve_builtin_seed_check( + app: &BuiltinMiniAppBundle, + installed: Option<&BuiltinInstallMarker>, +) -> BuiltinSeedCheck { + let artifacts = build_builtin_seed_artifacts(app); + if should_seed_builtin_app(app, &artifacts.content_hash, installed) { + BuiltinSeedCheck::NeedsSeed(artifacts) + } else { + BuiltinSeedCheck::Skip + } +} + +pub fn resolve_builtin_seed_action( + artifacts: BuiltinSeedArtifacts, + has_local_override: bool, +) -> BuiltinSeedAction { + if has_local_override { + BuiltinSeedAction::PreserveLocalOverride(artifacts) + } else { + BuiltinSeedAction::SeedBundle(artifacts) + } +} + +pub fn serialize_builtin_install_marker( + marker: &BuiltinInstallMarker, +) -> serde_json::Result { + serde_json::to_string_pretty(marker) +} + +pub fn parse_builtin_install_marker(content: &str) -> serde_json::Result { + serde_json::from_str(content) +} + pub fn should_seed_builtin_app( app: &BuiltinMiniAppBundle, content_hash: &str, diff --git a/src/crates/product-domains/tests/function_agent_contracts.rs b/src/crates/product-domains/tests/function_agent_contracts.rs index 146c02969..f9974a8f5 100644 --- a/src/crates/product-domains/tests/function_agent_contracts.rs +++ b/src/crates/product-domains/tests/function_agent_contracts.rs @@ -3,10 +3,10 @@ use bitfun_product_domains::function_agents::{ git_func_agent::{ assemble_commit_message, build_changes_summary_from_paths, build_commit_prompt, - detect_change_patterns, extract_module_name, infer_file_type, parse_commit_analysis_value, - parse_commit_type_label, prepare_commit_prompt, truncate_diff_for_commit_prompt, - ChangePattern, CommitFormat, CommitMessageOptions, CommitType, FileChange, FileChangeType, - ProjectContext, + detect_change_patterns, extract_module_name, infer_file_type, parse_commit_analysis_json, + parse_commit_analysis_value, parse_commit_type_label, prepare_commit_prompt, + truncate_diff_for_commit_prompt, ChangePattern, CommitFormat, CommitMessageOptions, + CommitType, FileChange, FileChangeType, ProjectContext, }, ports::{ CommitAiAnalysisRequest, FunctionAgentAiPort, FunctionAgentFuture, FunctionAgentGitPort, @@ -15,9 +15,10 @@ use bitfun_product_domains::function_agents::{ }, startchat_func_agent::{ build_complete_analysis_prompt, combine_git_diffs, limit_quick_actions, - normalize_predicted_actions, parse_complete_analysis_value, parse_git_status_porcelain, - parse_predicted_actions_from_values, parse_quick_actions_from_values, time_of_day_for_hour, - ActionPriority, AheadBehind, GitWorkState, QuickActionType, TimeOfDay, WorkStateOptions, + normalize_predicted_actions, parse_complete_analysis_json, parse_complete_analysis_value, + parse_git_status_porcelain, parse_predicted_actions_from_values, + parse_quick_actions_from_values, time_of_day_for_hour, ActionPriority, AheadBehind, + GitWorkState, QuickActionType, TimeOfDay, WorkStateOptions, }, AgentErrorType, Language, }; @@ -645,3 +646,56 @@ fn git_function_agent_utils_preserve_change_classification() { assert!(patterns.contains(&ChangePattern::BugFix)); assert!(patterns.contains(&ChangePattern::DocumentationUpdate)); } + +#[test] +fn function_agent_json_helpers_parse_ai_payloads_without_core_runtime() { + let commit = parse_commit_analysis_json( + r#"{ + "type": "refactor", + "title": "refactor(product-domains): move parse helpers", + "body": "Keep runtime adapters in core.", + "confidence": 0.92 + }"#, + ) + .unwrap(); + assert_eq!(commit.commit_type, CommitType::Refactor); + assert_eq!( + commit.title, + "refactor(product-domains): move parse helpers" + ); + assert_eq!( + commit.body.as_deref(), + Some("Keep runtime adapters in core.") + ); + assert_eq!(commit.confidence, 0.92); + + let missing_title = parse_commit_analysis_json(r#"{"type":"fix"}"#).unwrap_err(); + assert_eq!(missing_title, "Missing title field"); + + let invalid_commit = parse_commit_analysis_json("not json").unwrap_err(); + assert!(invalid_commit.starts_with("Failed to parse AI response:")); + + let work_state = parse_complete_analysis_json( + r#"{ + "summary": "Working on product-domain owner closure.", + "predicted_actions": [ + {"description": "Run checks", "priority": "High", "icon": "check", "is_reminder": false} + ], + "quick_actions": [ + {"title": "Status", "command": "git status", "icon": "git", "action_type": "ViewStatus"} + ] + }"#, + ) + .unwrap(); + assert_eq!( + work_state.analysis.summary, + "Working on product-domain owner closure." + ); + assert_eq!(work_state.predicted_actions_count, 1); + assert_eq!(work_state.quick_actions_count, 1); + assert_eq!(work_state.analysis.predicted_actions.len(), 3); + assert_eq!(work_state.analysis.quick_actions.len(), 1); + + let invalid_work_state = parse_complete_analysis_json("not json").unwrap_err(); + assert!(invalid_work_state.starts_with("Failed to parse complete analysis response:")); +} diff --git a/src/crates/product-domains/tests/miniapp_contracts.rs b/src/crates/product-domains/tests/miniapp_contracts.rs index 81e91717c..8f962c12e 100644 --- a/src/crates/product-domains/tests/miniapp_contracts.rs +++ b/src/crates/product-domains/tests/miniapp_contracts.rs @@ -2,9 +2,12 @@ use bitfun_product_domains::miniapp::bridge_builder::{build_bridge_script, build_csp_content}; use bitfun_product_domains::miniapp::builtin::{ - build_builtin_package_json, builtin_content_hash, builtin_source_files, - should_seed_builtin_app, BuiltinInstallMarker, BuiltinMiniAppBundle, BUILTIN_INSTALL_MARKER, - BUILTIN_PLACEHOLDER_COMPILED_HTML, LEGACY_BUILTIN_VERSION_MARKER, + build_builtin_install_marker, build_builtin_package_json, builtin_content_hash, + builtin_source_files, legacy_builtin_version_marker_content, parse_builtin_install_marker, + resolve_builtin_seed_action, resolve_builtin_seed_check, serialize_builtin_install_marker, + should_seed_builtin_app, BuiltinInstallMarker, BuiltinMiniAppBundle, BuiltinSeedAction, + BuiltinSeedCheck, BUILTIN_INSTALL_MARKER, BUILTIN_PLACEHOLDER_COMPILED_HTML, + LEGACY_BUILTIN_VERSION_MARKER, }; use bitfun_product_domains::miniapp::compiler::compile; use bitfun_product_domains::miniapp::customization::{ @@ -784,6 +787,61 @@ fn miniapp_builtin_contract_preserves_seed_marker_and_hash_policy() { ); } +#[test] +fn miniapp_builtin_contract_owns_seed_plan_and_marker_wire_shape() { + let app = BuiltinMiniAppBundle { + id: "builtin-demo", + version: 7, + meta_json: r#"{"id":"builtin-demo"}"#, + html: "", + css: "body { color: red; }", + ui_js: r#"console.log("ui");"#, + worker_js: r#"console.log("worker");"#, + esm_dependencies_json: "[]", + }; + let artifacts = bitfun_product_domains::miniapp::builtin::build_builtin_seed_artifacts(&app); + let marker = build_builtin_install_marker(&app, &artifacts.content_hash); + + assert_eq!(artifacts.marker, marker); + assert_eq!(artifacts.legacy_version, "7"); + assert_eq!(legacy_builtin_version_marker_content(&app), "7"); + assert_eq!( + resolve_builtin_seed_check(&app, Some(&marker)), + BuiltinSeedCheck::Skip + ); + + let stale_marker = BuiltinInstallMarker { + version: 7, + hash: "sha256:stale".to_string(), + }; + assert_eq!( + resolve_builtin_seed_check(&app, Some(&stale_marker)), + BuiltinSeedCheck::NeedsSeed(artifacts.clone()) + ); + assert_eq!( + resolve_builtin_seed_check(&app, None), + BuiltinSeedCheck::NeedsSeed(artifacts.clone()) + ); + assert_eq!( + resolve_builtin_seed_action(artifacts.clone(), true), + BuiltinSeedAction::PreserveLocalOverride(artifacts.clone()) + ); + assert_eq!( + resolve_builtin_seed_action(artifacts.clone(), false), + BuiltinSeedAction::SeedBundle(artifacts.clone()) + ); + + let serialized = serialize_builtin_install_marker(&marker).unwrap(); + assert_eq!( + serialized, + format!( + "{{\n \"version\": 7,\n \"hash\": \"{}\"\n}}", + artifacts.content_hash + ) + ); + assert_eq!(parse_builtin_install_marker(&serialized).unwrap(), marker); +} + #[test] fn miniapp_ports_keep_runtime_boundary_lightweight() { let decoded: MiniAppInstallDepsRequest = serde_json::from_value(serde_json::json!({