diff --git a/docs/architecture/core-decomposition.md b/docs/architecture/core-decomposition.md index fe484b08e..7e12a42ae 100644 --- a/docs/architecture/core-decomposition.md +++ b/docs/architecture/core-decomposition.md @@ -70,7 +70,7 @@ Rust 编译和链接面。 | `bitfun-tool-packs` | 由 feature group 隔离的具体工具实现 | target/scaffold:仅提供 basic / git / mcp / browser-web / computer-use / image-analysis / miniapp / agent-control feature-group 元数据,不得声明 concrete tools 已迁移 | | `bitfun-services-core` | Config、session、workspace、storage、filesystem、system services | partial:部分 pure helper 已迁出;config/workspace/filesystem runtime 多数仍在 core | | `bitfun-services-integrations` | Git、MCP、remote SSH、remote connect、file watch integrations | partial:MCP runtime 已迁入;remote SSH 仍只迁移低风险 contracts/helpers;remote-connect 已拥有 wire DTO、request builder、tracker state / registry lifecycle 与 tracker event reduction,dispatcher/product execution 仍在 core | -| `bitfun-product-domains` | Miniapp 和 function-agent 产品子域 | partial:pure decision、port、storage layout 可迁入;IO、worker、Git/AI service runtime 仍在 core | +| `bitfun-product-domains` | Miniapp 和 function-agent 产品子域 | partial:pure decision、port、storage/builtin contract 可迁入;IO、worker、built-in asset seeding、Git/AI service runtime 仍在 core | | `terminal-core` | 已有 terminal package,移动到 workspace 顶层 `src/crates/terminal` 路径 | done:已在 workspace 顶层 | | `tool-runtime` | 已有 tool runtime,移动到 workspace 顶层路径 | done:已在 workspace 顶层 | @@ -187,11 +187,12 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate 仍由 `product-full` 启用、把只依赖 port 的 helper 迁入 owner 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 等纯决策 / 解析逻辑;实际 runtime detection、worker pool、 + customization metadata / permission diff、built-in MiniApp bundle/hash/marker/source payload + seed-decision contract 等纯决策 / 解析逻辑;实际 runtime detection、worker pool、 storage IO、PathManager、进程执行、host dispatch 执行、customization draft 存储 / 应用与 builtin - asset seeding 仍留在 core product runtime。最新内置 PR Review MiniApp 依赖 core - `BuiltinApp` seed、content hash、install marker 与 customized update metadata;这些不是 - `product-domains` runtime owner 已迁移的证据。 + asset include / seeding / marker IO / recompile 仍留在 core product runtime。最新内置 PR Review + MiniApp 依赖 core asset include、user-data seed、customized update runtime 与 source-hash input + lookup;这些不是 `product-domains` runtime owner 已迁移的证据。 - `product-domains` 可以先定义 MiniApp runtime/storage 与 function-agent Git/AI 的 port contract,并承载 function-agent 的纯 prompt / AI response parsing policy;core-owned adapter 只能在不改变执行路径的前提下委托现有 service,并先补等价测试。IO/进程/AI/Git 执行 owner @@ -205,9 +206,12 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate migration-before snapshots for core-owned MiniApp import / sync / recompile / rollback / dependency state paths and function-agent Git / AI response boundaries. Core still owns MiniApp filesystem IO, worker process execution, - built-in asset seeding/source-hash lookup, host-dispatch execution, + 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. + 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 + asset includes, marker IO, customized update runtime, and recompile + orchestration remain core-owned. - 高风险:`ToolUseContext`、product tool registry / runtime manifest assembly / `GetToolSpec` 执行 owner 化、 MCP concrete tool integration、remote-connect、remote SSH runtime、miniapp / function-agent runtime、 agent registry、`bitfun-core default = []` @@ -261,7 +265,7 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate adapter 接入该 facade;Startchat 接线必须保留 legacy git-state、git-diff fallback、 `analyze_git=false` time-info 与 `analyzed_at` 时序。 core 仍持有 MiniApp filesystem IO、compiler 调度、worker process、host dispatch、 - built-in seed/update,以及 function-agent Git/AI service adapter、prompt template、 + built-in asset include / seed / marker IO / recompile,以及 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 0bd93a643..bd2d749d5 100644 --- a/docs/plans/core-decomposition-plan.md +++ b/docs/plans/core-decomposition-plan.md @@ -1128,8 +1128,9 @@ 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, 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. -- 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 已完成迁移。 +- 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 已完成迁移。 - miniapp runtime/storage/manager/host dispatch/exporter/builtin 与 function-agent 运行逻辑继续迁移前,需要先确认 agent/tool/provider port 和 Git/AI service 边界。 **验证:** @@ -1543,7 +1544,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、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 与 function-agent staged diff / AI response error mapping 的迁移前快照。PathManager、Git/AI service、prompt template、builtin asset seeding、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、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 仍未迁移。 - 本次 P2 后续复核结论:上述高耦合剩余项不是纯文件搬迁;若继续迁移会改变依赖方向或需要新增 port/provider 行为合约。因此当前 PR 将它们显式保留为 core-owned runtime,只完成低风险 owner container 化,并通过 boundary check 防止已拆 owner crate 回流依赖 core。 **后续风险重排(2026-05-13):** @@ -1669,7 +1670,7 @@ P2 后产品表面契约轨道(contract-only): - 最新主干的 Web 启动性能优化新增 startup trace、deferred background scheduler、narrow tool initializer、Monaco warmup 与历史会话非阻塞 hydrate;这些属于 web app / Flow Chat product surface,不是 core service 迁移前置条件。后续只能通过 web product checks 验证,不得把 `startupTrace`、`backgroundTaskScheduler`、history hydration 或 tool warmup 下沉到 core-types / runtime-ports / agent-tools。 - 最新主干的 CLI 重构主要新增 TUI/theme/selector/dialog/chat-state 等 app-layer 代码,后续又收敛预置 theme、增加 mode-aware subagent management,并补充 desktop companion pet resize / Windows UX;这些当前没有改变 `services-integrations` 的迁移归属。后续若调整 shared crate 边界,必须继续把 `bitfun-cli`、`ratatui`、`crossterm`、`arboard`、`syntect-tui` 等 CLI-only 依赖限制在 app adapter / presentation layer,desktop / web-ui presentation 修复也不应被误判为 core service 迁移前置条件。 - 最新 desktop close button 默认最小化到 system tray 属于 desktop lifecycle surface;后续 desktop app lifecycle / window state 调整只能通过 desktop product check 验证,不作为 core service owner 外移前置条件。 -- 最新主干的内置 PR Review MiniApp 通过 core `BuiltinApp` asset bundle、content hash、install marker、customization metadata 与 update marker seed 到用户数据目录;它可以复用 `product-domains` 的 MiniApp 纯 contract,但 builtin asset seeding / customized update runtime 仍显式 core-owned,迁移前必须保留这些行为的等价测试。 +- 最新主干的内置 PR Review MiniApp 通过 core asset include、customization metadata IO、marker IO 与 update marker seed 到用户数据目录;它复用 `product-domains` 的 built-in bundle/hash/marker seed-decision contract,但 builtin asset seeding / customized update runtime / recompile orchestration 仍显式 core-owned,迁移前必须保留这些行为的等价测试。 - P2 后产品表面策略要求“surface divergence, capability convergence”:CLI `/diff`、Desktop 快捷键/面板、Remote card、ACP method 可以映射到同一 capability contract,但不能为了复用把 surface command 或 UI rendering 下沉到 contract crate。 - `ToolUseContext` 的 shared-context / evidence checkpoint hook、`TaskTool` / `CodeReviewTool` 的 Deep Review capacity flow、session manifest/cache persistence、rollback persisted-turn cleanup、search fallback chain 与 stream finish/tool-call contract 不能在 P3 中只通过 re-export 消失;如果外移,需要先补 boundary contract、旧路径兼容和对应 regression。 - P3 的闭环检查应同时覆盖 Rust crate graph 与产品 runtime 行为:边界脚本只证明依赖方向,不能替代 Deep Review、MCP dynamic tools、tool manifest / `GetToolSpec`、remote connect、snapshot wrapping、miniapp/function-agent 的产品等价性验证。 diff --git a/scripts/check-core-boundaries.mjs b/scripts/check-core-boundaries.mjs index c9a796b6c..cef810f05 100644 --- a/scripts/check-core-boundaries.mjs +++ b/scripts/check-core-boundaries.mjs @@ -1877,23 +1877,43 @@ const requiredContentRules = [ { path: 'src/crates/core/src/miniapp/builtin/mod.rs', reason: - 'core must continue owning built-in MiniApp asset seeding and update markers until builtin asset runtime migration is reviewed', + 'core must continue owning built-in MiniApp asset includes, seeding IO, marker writes, and recompilation until builtin asset runtime migration is reviewed', patterns: [ { regex: /id: "builtin-pr-review"/, message: 'missing built-in PR Review MiniApp anchor', }, { - regex: /\bstruct BuiltinInstallMarker\b/, - message: 'missing built-in MiniApp install marker', + regex: /\bBUILTIN_APPS\b/, + message: 'missing built-in MiniApp asset include owner', }, { - regex: /\bfn builtin_content_hash\b/, - message: 'missing built-in MiniApp content hash policy', + regex: /\bbuiltin_content_hash\b/, + message: 'missing product-domain built-in MiniApp content hash use', }, { - regex: /\bfn should_seed_builtin_app\b/, - message: 'missing built-in MiniApp seed decision policy', + regex: /\bshould_seed_builtin_app\b/, + message: 'missing product-domain built-in MiniApp seed decision use', + }, + { + regex: /\bbuiltin_source_files\b/, + message: 'missing product-domain built-in MiniApp source payload use', + }, + { + regex: /\bBUILTIN_PLACEHOLDER_COMPILED_HTML\b/, + message: 'missing product-domain built-in MiniApp placeholder payload use', + }, + { + regex: /\bread_builtin_install_marker\b/, + message: 'missing core-owned built-in MiniApp marker read IO', + }, + { + regex: /\bwrite_builtin_install_marker\b/, + message: 'missing core-owned built-in MiniApp marker write IO', + }, + { + regex: /\brecompile\b/, + message: 'missing core-owned built-in MiniApp recompile orchestration', }, { regex: /\bload_customization_metadata\b/, @@ -2273,6 +2293,45 @@ const requiredContentRules = [ }, ], }, + { + path: 'src/crates/product-domains/src/miniapp/builtin.rs', + reason: + 'product-domains owns pure built-in MiniApp bundle, marker, hash, and seed-decision contracts while core keeps asset seeding IO and recompilation', + patterns: [ + { + regex: /\bpub struct BuiltinMiniAppBundle\b/, + message: 'missing built-in MiniApp bundle contract', + }, + { + regex: /\bpub struct BuiltinInstallMarker\b/, + message: 'missing built-in MiniApp install marker contract', + }, + { + regex: /\bpub const BUILTIN_INSTALL_MARKER\b/, + message: 'missing built-in MiniApp marker filename contract', + }, + { + regex: /\bpub fn builtin_content_hash\b/, + message: 'missing built-in MiniApp content hash helper', + }, + { + regex: /\bpub fn should_seed_builtin_app\b/, + message: 'missing built-in MiniApp seed decision helper', + }, + { + regex: /\bpub fn builtin_source_files\b/, + message: 'missing built-in MiniApp source payload helper', + }, + { + regex: /\bpub const BUILTIN_PLACEHOLDER_COMPILED_HTML\b/, + message: 'missing built-in MiniApp placeholder payload contract', + }, + { + regex: /\bpub fn build_builtin_package_json\b/, + message: 'missing built-in MiniApp package payload helper', + }, + ], + }, { path: 'src/crates/core/src/function_agents/startchat-func-agent/work_state_analyzer.rs', reason: @@ -3081,13 +3140,31 @@ function runManifestParserSelfTest() { path: 'src/crates/core/src/miniapp/builtin/mod.rs', contracts: [ 'builtin-pr-review', - 'BuiltinInstallMarker', + 'BUILTIN_APPS', 'builtin_content_hash', 'should_seed_builtin_app', + 'builtin_source_files', + 'BUILTIN_PLACEHOLDER_COMPILED_HTML', + 'read_builtin_install_marker', + 'write_builtin_install_marker', + 'recompile', 'load_customization_metadata', 'available_builtin_update', ], }, + { + path: 'src/crates/product-domains/src/miniapp/builtin.rs', + contracts: [ + 'BuiltinMiniAppBundle', + 'BuiltinInstallMarker', + 'BUILTIN_INSTALL_MARKER', + 'builtin_content_hash', + 'should_seed_builtin_app', + 'builtin_source_files', + 'BUILTIN_PLACEHOLDER_COMPILED_HTML', + 'build_builtin_package_json', + ], + }, { path: 'src/crates/core/src/miniapp/host_dispatch.rs', contracts: [ diff --git a/src/crates/core/AGENTS.md b/src/crates/core/AGENTS.md index 6ed1c7f73..4001fb9a9 100644 --- a/src/crates/core/AGENTS.md +++ b/src/crates/core/AGENTS.md @@ -56,6 +56,11 @@ SessionManager → Session → DialogTurn → ModelRound 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 + 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 small port/interface boundary. - Do not move platform-specific logic, build-script behavior, or product diff --git a/src/crates/core/src/miniapp/builtin/mod.rs b/src/crates/core/src/miniapp/builtin/mod.rs index 3869e7648..e4ca41946 100644 --- a/src/crates/core/src/miniapp/builtin/mod.rs +++ b/src/crates/core/src/miniapp/builtin/mod.rs @@ -8,35 +8,17 @@ 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, +}; use chrono::Utc; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; use std::path::Path; use std::sync::Arc; -const BUILTIN_MARKER: &str = ".builtin-manifest.json"; -const LEGACY_BUILTIN_VERSION_MARKER: &str = ".builtin-version"; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -struct BuiltinInstallMarker { - version: u32, - hash: String, -} - /// A built-in MiniApp bundled with the application binary. -#[derive(Clone, Copy)] -pub struct BuiltinApp { - /// Stable id used as on-disk directory name (also exposed in the gallery). - pub id: &'static str, - /// Schema version for migration-sensitive changes. Asset changes are detected by hash. - pub version: u32, - pub meta_json: &'static str, - pub html: &'static str, - pub css: &'static str, - pub ui_js: &'static str, - pub worker_js: &'static str, - pub esm_dependencies_json: &'static str, -} +pub type BuiltinApp = BuiltinMiniAppBundle; /// All built-in apps that ship with BitFun. pub const BUILTIN_APPS: &[BuiltinApp] = &[ @@ -107,11 +89,11 @@ 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_MARKER); + let marker_path = app_dir.join(BUILTIN_INSTALL_MARKER); let content_hash = builtin_content_hash(app); if let Some(marker) = read_builtin_install_marker(&marker_path).await? { - if !should_seed_builtin_app_with_hash(app, &content_hash, Some(&marker)) { + if !should_seed_builtin_app(app, &content_hash, Some(&marker)) { return Ok(()); } } @@ -184,22 +166,12 @@ async fn seed_one(manager: &Arc, app: &BuiltinApp) -> BitFunResu .map_err(|e| BitFunError::io(format!("write meta.json failed: {}", e)))?; // Source files (always overwrite). - write_file(source_dir.join("index.html"), app.html).await?; - write_file(source_dir.join("style.css"), app.css).await?; - write_file(source_dir.join("ui.js"), app.ui_js).await?; - write_file(source_dir.join("worker.js"), app.worker_js).await?; - write_file( - source_dir.join("esm_dependencies.json"), - app.esm_dependencies_json, - ) - .await?; + for (file_name, content) in builtin_source_files(app) { + write_file(source_dir.join(file_name), content).await?; + } // package.json — overwrite with empty deps; built-in apps must not require npm install. - let pkg = serde_json::json!({ - "name": format!("miniapp-{}", app.id), - "private": true, - "dependencies": {} - }); + let pkg = build_builtin_package_json(app.id); let pkg_json = serde_json::to_string_pretty(&pkg).map_err(BitFunError::from)?; write_file(app_dir.join("package.json"), &pkg_json).await?; @@ -212,7 +184,7 @@ async fn seed_one(manager: &Arc, app: &BuiltinApp) -> BitFunResu // Placeholder compiled.html so storage::load() doesn't fail before recompile. write_file( app_dir.join("compiled.html"), - "Loading...", + BUILTIN_PLACEHOLDER_COMPILED_HTML, ) .await?; @@ -238,46 +210,6 @@ async fn seed_one(manager: &Arc, app: &BuiltinApp) -> BitFunResu Ok(()) } -fn builtin_content_hash(app: &BuiltinApp) -> String { - let mut hasher = Sha256::new(); - hash_builtin_asset(&mut hasher, "meta.json", app.meta_json); - hash_builtin_asset(&mut hasher, "index.html", app.html); - hash_builtin_asset(&mut hasher, "style.css", app.css); - hash_builtin_asset(&mut hasher, "ui.js", app.ui_js); - hash_builtin_asset(&mut hasher, "worker.js", app.worker_js); - hash_builtin_asset( - &mut hasher, - "esm_dependencies.json", - app.esm_dependencies_json, - ); - format!("sha256:{}", hex::encode(hasher.finalize())) -} - -fn hash_builtin_asset(hasher: &mut Sha256, name: &str, content: &str) { - hasher.update(name.as_bytes()); - hasher.update([0u8]); - hasher.update(content.len().to_le_bytes()); - hasher.update([0u8]); - hasher.update(content.as_bytes()); -} - -#[cfg(test)] -fn should_seed_builtin_app(app: &BuiltinApp, installed: Option<&BuiltinInstallMarker>) -> bool { - let content_hash = builtin_content_hash(app); - should_seed_builtin_app_with_hash(app, &content_hash, installed) -} - -fn should_seed_builtin_app_with_hash( - app: &BuiltinApp, - content_hash: &str, - installed: Option<&BuiltinInstallMarker>, -) -> bool { - !matches!( - installed, - Some(marker) if marker.version >= app.version && marker.hash == content_hash - ) -} - async fn read_builtin_install_marker(path: &Path) -> BitFunResult> { let content = match tokio::fs::read_to_string(path).await { Ok(content) => content, @@ -337,7 +269,7 @@ mod tests { async fn write_outdated_builtin_marker(app_dir: &std::path::Path) { write_builtin_install_marker( - &app_dir.join(BUILTIN_MARKER), + &app_dir.join(BUILTIN_INSTALL_MARKER), &BuiltinInstallMarker { version: 0, hash: "sha256:outdated".to_string(), @@ -849,18 +781,31 @@ mod tests { version: app.version, hash: builtin_content_hash(app), }; + let content_hash = builtin_content_hash(app); let stale_hash_marker = BuiltinInstallMarker { version: app.version, hash: "sha256:stale".to_string(), }; let older_version_marker = BuiltinInstallMarker { version: app.version.saturating_sub(1), - hash: builtin_content_hash(app), + hash: content_hash.clone(), }; - assert!(!should_seed_builtin_app(app, Some(¤t_marker))); - assert!(should_seed_builtin_app(app, Some(&stale_hash_marker))); - assert!(should_seed_builtin_app(app, Some(&older_version_marker))); - assert!(should_seed_builtin_app(app, None)); + assert!(!should_seed_builtin_app( + app, + &content_hash, + Some(¤t_marker) + )); + assert!(should_seed_builtin_app( + app, + &content_hash, + Some(&stale_hash_marker) + )); + assert!(should_seed_builtin_app( + app, + &content_hash, + Some(&older_version_marker) + )); + assert!(should_seed_builtin_app(app, &content_hash, None)); } } diff --git a/src/crates/product-domains/AGENTS.md b/src/crates/product-domains/AGENTS.md index b305875f1..593795cd0 100644 --- a/src/crates/product-domains/AGENTS.md +++ b/src/crates/product-domains/AGENTS.md @@ -37,17 +37,18 @@ 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, port traits, and storage-backed runtime - state facade logic. + 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. - `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. - 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 service adapters, prompt templates, JSON - extraction, and error mapping. + execution, built-in asset includes/seeding, marker IO, recompile orchestration, + source-hash input lookup, `PathManager` integration, function-agent Git/AI + service adapters, prompt templates, JSON extraction, and error mapping. ## Verification diff --git a/src/crates/product-domains/Cargo.toml b/src/crates/product-domains/Cargo.toml index e20bb8f8c..2de4a51eb 100644 --- a/src/crates/product-domains/Cargo.toml +++ b/src/crates/product-domains/Cargo.toml @@ -14,9 +14,10 @@ serde = { workspace = true } serde_json = { workspace = true } dirs = { workspace = true, optional = true } log = { workspace = true, optional = true } +sha2 = { workspace = true, optional = true } [features] default = [] -miniapp = ["dirs"] +miniapp = ["dirs", "sha2"] function-agents = ["log"] product-full = ["miniapp", "function-agents"] diff --git a/src/crates/product-domains/src/miniapp/builtin.rs b/src/crates/product-domains/src/miniapp/builtin.rs new file mode 100644 index 000000000..3b8ac7ccc --- /dev/null +++ b/src/crates/product-domains/src/miniapp/builtin.rs @@ -0,0 +1,90 @@ +//! Built-in MiniApp bundle contracts and pure seed policy. + +use crate::miniapp::storage::{ + build_package_json, ESM_DEPS_JSON, INDEX_HTML, STYLE_CSS, UI_JS, WORKER_JS, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +pub const BUILTIN_INSTALL_MARKER: &str = ".builtin-manifest.json"; +pub const LEGACY_BUILTIN_VERSION_MARKER: &str = ".builtin-version"; +pub const BUILTIN_PLACEHOLDER_COMPILED_HTML: &str = + "Loading..."; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuiltinInstallMarker { + pub version: u32, + pub hash: String, +} + +/// 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)] +pub struct BuiltinMiniAppBundle { + pub id: &'static str, + pub version: u32, + pub meta_json: &'static str, + pub html: &'static str, + pub css: &'static str, + pub ui_js: &'static str, + pub worker_js: &'static str, + pub esm_dependencies_json: &'static str, +} + +pub fn builtin_content_hash(app: &BuiltinMiniAppBundle) -> String { + let mut hasher = Sha256::new(); + hash_builtin_asset(&mut hasher, "meta.json", app.meta_json); + hash_builtin_asset(&mut hasher, "index.html", app.html); + hash_builtin_asset(&mut hasher, "style.css", app.css); + hash_builtin_asset(&mut hasher, "ui.js", app.ui_js); + hash_builtin_asset(&mut hasher, "worker.js", app.worker_js); + hash_builtin_asset( + &mut hasher, + "esm_dependencies.json", + app.esm_dependencies_json, + ); + format!("sha256:{}", hex_encode(&hasher.finalize())) +} + +pub fn should_seed_builtin_app( + app: &BuiltinMiniAppBundle, + content_hash: &str, + installed: Option<&BuiltinInstallMarker>, +) -> bool { + !matches!( + installed, + Some(marker) if marker.version >= app.version && marker.hash == content_hash + ) +} + +pub fn build_builtin_package_json(app_id: &str) -> serde_json::Value { + build_package_json(app_id, &[]) +} + +pub fn builtin_source_files(app: &BuiltinMiniAppBundle) -> [(&'static str, &'static str); 5] { + [ + (INDEX_HTML, app.html), + (STYLE_CSS, app.css), + (UI_JS, app.ui_js), + (WORKER_JS, app.worker_js), + (ESM_DEPS_JSON, app.esm_dependencies_json), + ] +} + +fn hash_builtin_asset(hasher: &mut Sha256, name: &str, content: &str) { + hasher.update(name.as_bytes()); + hasher.update([0u8]); + hasher.update(content.len().to_le_bytes()); + hasher.update([0u8]); + hasher.update(content.as_bytes()); +} + +fn hex_encode(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut output = String::with_capacity(bytes.len() * 2); + for byte in bytes { + output.push(HEX[(byte >> 4) as usize] as char); + output.push(HEX[(byte & 0x0f) as usize] as char); + } + output +} diff --git a/src/crates/product-domains/src/miniapp/mod.rs b/src/crates/product-domains/src/miniapp/mod.rs index 2e64af696..c6a328c4b 100644 --- a/src/crates/product-domains/src/miniapp/mod.rs +++ b/src/crates/product-domains/src/miniapp/mod.rs @@ -1,6 +1,7 @@ //! MiniApp domain contracts and pure helpers. pub mod bridge_builder; +pub mod builtin; pub mod compiler; pub mod customization; pub mod draft; diff --git a/src/crates/product-domains/tests/miniapp_contracts.rs b/src/crates/product-domains/tests/miniapp_contracts.rs index 48d1a4562..81e91717c 100644 --- a/src/crates/product-domains/tests/miniapp_contracts.rs +++ b/src/crates/product-domains/tests/miniapp_contracts.rs @@ -1,6 +1,11 @@ #![cfg(feature = "miniapp")] 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, +}; use bitfun_product_domains::miniapp::compiler::compile; use bitfun_product_domains::miniapp::customization::{ apply_draft_customization_metadata, decline_builtin_update_metadata, @@ -710,6 +715,75 @@ fn miniapp_storage_import_fallback_contract_remains_stable() { assert_eq!(fallbacks.package_json, package); } +#[test] +fn miniapp_builtin_contract_preserves_seed_marker_and_hash_policy() { + let app = BuiltinMiniAppBundle { + id: "builtin-demo", + version: 2, + 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 content_hash = builtin_content_hash(&app); + + assert_eq!(BUILTIN_INSTALL_MARKER, ".builtin-manifest.json"); + assert_eq!(LEGACY_BUILTIN_VERSION_MARKER, ".builtin-version"); + assert_eq!( + content_hash, + "sha256:5a2625011813ed9f39eea6875ab96047eb383ac005298ea86ce68e5ac4e79825" + ); + + assert!(should_seed_builtin_app(&app, &content_hash, None)); + assert!(!should_seed_builtin_app( + &app, + &content_hash, + Some(&BuiltinInstallMarker { + version: 2, + hash: content_hash.clone(), + }), + )); + assert!(should_seed_builtin_app( + &app, + &content_hash, + Some(&BuiltinInstallMarker { + version: 1, + hash: content_hash.clone(), + }), + )); + assert!(should_seed_builtin_app( + &app, + &content_hash, + Some(&BuiltinInstallMarker { + version: 3, + hash: "sha256:old".to_string(), + }), + )); + + let package = build_builtin_package_json(app.id); + assert_eq!(package["name"], "miniapp-builtin-demo"); + assert_eq!(package["private"], true); + assert_eq!(package["dependencies"], serde_json::json!({})); + + let source_files = builtin_source_files(&app); + assert_eq!( + source_files, + [ + (INDEX_HTML, app.html), + (STYLE_CSS, app.css), + (UI_JS, app.ui_js), + (WORKER_JS, app.worker_js), + (ESM_DEPS_JSON, app.esm_dependencies_json), + ] + ); + assert_eq!( + BUILTIN_PLACEHOLDER_COMPILED_HTML, + "Loading..." + ); +} + #[test] fn miniapp_ports_keep_runtime_boundary_lightweight() { let decoded: MiniAppInstallDepsRequest = serde_json::from_value(serde_json::json!({