From 661927f0c78cc09972a42a76b90c9d0d30adc2ad Mon Sep 17 00:00:00 2001 From: limityan Date: Sun, 2 Aug 2026 18:52:04 +0800 Subject: [PATCH] perf(build): close Core feature dependency profiles Split lightweight service and SSH facade features from full product composition, remove redundant Core dependency ownership, and guard the recursive feature closure with boundary tests. --- Cargo.lock | 12 --- scripts/check-core-boundaries.test.mjs | 31 +++++++ scripts/core-boundaries/checker.mjs | 61 +++++++++++++ .../manifest-feature-helpers.mjs | 31 +++++++ scripts/core-boundaries/rules/crate-rules.mjs | 4 + .../core-boundaries/rules/feature-rules.mjs | 79 ++++++++++++---- .../rules/source/required-rules.mjs | 37 +++++--- scripts/core-boundaries/self-test.mjs | 91 +++++++++++++++---- src/crates/assembly/core/AGENTS.md | 6 ++ src/crates/assembly/core/Cargo.toml | 80 ++++++---------- .../assembly/core/src/external_hooks.rs | 6 +- .../core/src/infrastructure/events/mod.rs | 3 + .../assembly/core/src/infrastructure/mod.rs | 1 + src/crates/assembly/core/src/lib.rs | 3 +- .../src/product_runtime/runtime_services.rs | 31 +++---- .../core/src/service/announcement/remote.rs | 16 ++-- .../core/src/service/config/service.rs | 2 +- .../core/src/service/dispatch/controller.rs | 8 +- .../assembly/core/src/service/dispatch/mod.rs | 41 ++++----- src/crates/assembly/core/src/service/mod.rs | 22 ++--- .../core/src/service/workspace/manager.rs | 6 +- .../core/src/service/workspace/mod.rs | 4 +- .../core/src/service/workspace/service.rs | 8 +- src/crates/assembly/core/src/util/errors.rs | 4 +- .../tests/remote_connect_host_boundary.rs | 2 +- 25 files changed, 398 insertions(+), 191 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 447ef7f165..46eea98cbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -963,7 +963,6 @@ dependencies = [ name = "bitfun-core" version = "0.2.15" dependencies = [ - "aes-gcm", "anyhow", "async-trait", "axum", @@ -994,15 +993,11 @@ dependencies = [ "dashmap", "dirs 6.0.0", "dunce", - "eventsource-stream", "filetime", "flate2", "fluent-bundle", "fs2", "futures", - "git2", - "glob", - "globset", "hex", "image 0.25.10", "include_dir", @@ -1010,21 +1005,15 @@ dependencies = [ "log", "md5", "notify", - "rand 0.8.7", "regex", "reqwest", "rmcp", "rusqlite", - "russh", - "rustls", - "rustls-native-certs", - "schannel", "serde", "serde_json", "serde_yaml", "sha2", "similar", - "sse-stream", "tempfile", "terminal-core", "thiserror 2.0.19", @@ -1037,7 +1026,6 @@ dependencies = [ "unic-langid", "urlencoding", "uuid", - "win32job", ] [[package]] diff --git a/scripts/check-core-boundaries.test.mjs b/scripts/check-core-boundaries.test.mjs index 14a34b8465..fa43204ebc 100644 --- a/scripts/check-core-boundaries.test.mjs +++ b/scripts/check-core-boundaries.test.mjs @@ -794,3 +794,34 @@ test('optional dependency ownership rejects undeclared direct feature owners', a ['missing', 'feature-ref'], ); }); + +test('closed feature profiles reject product-full hidden behind a child feature', async () => { + const { unexpectedReachableLocalFeatures } = await import( + './core-boundaries/manifest-feature-helpers.mjs' + ); + const features = new Map([ + ['service-integrations', { refs: ['announcement'], line: 1 }], + [ + 'announcement', + { + refs: ['bitfun-services-integrations/announcement', 'product-full'], + line: 2, + }, + ], + ['product-full', { refs: ['dep:rmcp'], line: 3 }], + ]); + + assert.deepEqual( + unexpectedReachableLocalFeatures( + features, + 'service-integrations', + new Set(['announcement']), + ), + [ + { + featureName: 'product-full', + path: ['service-integrations', 'announcement', 'product-full'], + }, + ], + ); +}); diff --git a/scripts/core-boundaries/checker.mjs b/scripts/core-boundaries/checker.mjs index b5df20f508..3c7e77a2b5 100644 --- a/scripts/core-boundaries/checker.mjs +++ b/scripts/core-boundaries/checker.mjs @@ -15,6 +15,7 @@ import { cratePathForName, } from './rules/crate-layout.mjs'; import { + coreClosedFeatureProfileRules, coreProductFullFeatureAssemblyRule, optionalDependencyFeatureOwnerRules, ownerCrateFeatureAssemblyRules, @@ -32,6 +33,7 @@ import { featureReferencesDependency, featureReferencesFeature, unexpectedDependencyOwnerFeatures, + unexpectedReachableLocalFeatures, } from './manifest-feature-helpers.mjs'; import { checkCargoDependencyBoundariesSafely } from './cargo-dependency-boundaries.mjs'; @@ -592,6 +594,61 @@ function checkCoreProductFullFeatureAssembly(rule) { } } +function checkClosedFeatureProfile(rule) { + const manifestPath = repoPathToFsPath(rule.manifestPath); + const features = parseManifestFeatures(readText(manifestPath).split(/\r?\n/)); + const feature = features.get(rule.featureName); + if (!feature) { + failures.push({ + path: manifestPath, + line: 1, + message: `${rule.reason}; missing ${rule.featureName} feature declaration`, + }); + return; + } + + for (const reference of rule.requiredFeatureRefs) { + if (!feature.refs.includes(reference)) { + failures.push({ + path: manifestPath, + line: feature.line, + message: `${rule.reason}; ${rule.featureName} must explicitly enable ${reference}`, + }); + } + } + + if (!rule.exact) { + return; + } + const allowedReferences = new Set(rule.requiredFeatureRefs); + for (const reference of feature.refs) { + if (!allowedReferences.has(reference)) { + failures.push({ + path: manifestPath, + line: feature.line, + message: `${rule.reason}; ${rule.featureName} must not enable ${reference}`, + }); + } + } + + const allowedLocalFeatures = new Set( + rule.requiredFeatureRefs.filter((reference) => features.has(reference)), + ); + for (const unexpected of unexpectedReachableLocalFeatures( + features, + rule.featureName, + allowedLocalFeatures, + )) { + failures.push({ + path: manifestPath, + line: features.get(unexpected.featureName)?.line ?? feature.line, + message: + `${rule.reason}; ${rule.featureName} must not reach local feature ` + + `${unexpected.featureName} via ${unexpected.path.join(' -> ')}`, + }); + } +} + function checkOwnerCrateFeatureAssembly(rule) { const manifestPath = repoPathToFsPath(rule.manifestPath); const features = parseManifestFeatures(readText(manifestPath).split(/\r?\n/)); @@ -1004,6 +1061,7 @@ export function runCoreBoundaryCheck() { parseManifestDependencies, manifestDependencyMatches, matchingForbiddenDependency, + coreClosedFeatureProfileRules, coreProductFullFeatureAssemblyRule, ownerCrateFeatureAssemblyRules, parseManifestFeatures, @@ -1064,6 +1122,9 @@ export function runCoreBoundaryCheck() { checkCoreDefaultProductFullFeature(); checkCoreProductFullFeatureAssembly(coreProductFullFeatureAssemblyRule); + for (const rule of coreClosedFeatureProfileRules) { + checkClosedFeatureProfile(rule); + } for (const rule of ownerCrateFeatureAssemblyRules) { checkOwnerCrateFeatureAssembly(rule); } diff --git a/scripts/core-boundaries/manifest-feature-helpers.mjs b/scripts/core-boundaries/manifest-feature-helpers.mjs index 8e3c257b68..4445e20707 100644 --- a/scripts/core-boundaries/manifest-feature-helpers.mjs +++ b/scripts/core-boundaries/manifest-feature-helpers.mjs @@ -21,3 +21,34 @@ export function unexpectedDependencyOwnerFeatures(features, dependency) { && !dependency.ownerFeatures.includes(featureName), ); } + +export function unexpectedReachableLocalFeatures( + features, + rootFeatureName, + allowedFeatureNames, +) { + const unexpected = []; + const visited = new Set([rootFeatureName]); + const pending = [{ featureName: rootFeatureName, path: [rootFeatureName] }]; + + while (pending.length > 0) { + const current = pending.shift(); + const feature = features.get(current.featureName); + if (!feature) { + continue; + } + for (const reference of feature.refs) { + if (!features.has(reference) || visited.has(reference)) { + continue; + } + visited.add(reference); + const path = [...current.path, reference]; + if (!allowedFeatureNames.has(reference)) { + unexpected.push({ featureName: reference, path }); + } + pending.push({ featureName: reference, path }); + } + } + + return unexpected; +} diff --git a/scripts/core-boundaries/rules/crate-rules.mjs b/scripts/core-boundaries/rules/crate-rules.mjs index 0cde3b3d49..4de5514f83 100644 --- a/scripts/core-boundaries/rules/crate-rules.mjs +++ b/scripts/core-boundaries/rules/crate-rules.mjs @@ -436,10 +436,14 @@ export const dependencyProfileRules = [ 'readability-js', 'rmcp', 'russh', + 'rustls', + 'rustls-native-certs', + 'schannel', 'sse-stream', 'similar', 'tool-runtime', 'tokio-tungstenite', + 'win32job', 'x25519-dalek', ], }, diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs index e2eb8b8cb7..322355cd9c 100644 --- a/scripts/core-boundaries/rules/feature-rules.mjs +++ b/scripts/core-boundaries/rules/feature-rules.mjs @@ -22,34 +22,30 @@ export const optionalDependencyFeatureOwnerRules = [ reason: 'bitfun-core product/runtime optional dependencies must stay owned by explicit feature gates', dependencies: [ - { depName: 'aes-gcm', ownerFeatures: ['service-integrations'] }, - { depName: 'axum', ownerFeatures: ['service-integrations'] }, + { depName: 'axum', ownerFeatures: ['product-full'] }, { depName: 'bitfun-ai-adapters', ownerFeatures: ['ai-adapter-runtime'] }, + { depName: 'bitfun-agent-runtime', ownerFeatures: ['product-full'] }, + { depName: 'bitfun-agent-stream', ownerFeatures: ['product-full'] }, + { depName: 'bitfun-harness', ownerFeatures: ['product-full'] }, { depName: 'bitfun-product-capabilities', ownerFeatures: ['product-capabilities'] }, { depName: 'bitfun-product-domains', ownerFeatures: ['product-domains'] }, + { depName: 'bitfun-runtime-services', ownerFeatures: ['runtime-services'] }, { depName: 'bitfun-tool-packs', ownerFeatures: ['tool-packs'] }, { depName: 'chrono-tz', ownerFeatures: ['product-full'] }, { depName: 'cron', ownerFeatures: ['product-full'] }, { depName: 'dashmap', ownerFeatures: ['product-full'] }, - { depName: 'eventsource-stream', ownerFeatures: ['product-full'] }, { depName: 'filetime', ownerFeatures: ['product-full'] }, { depName: 'flate2', ownerFeatures: ['product-full'] }, { depName: 'fs2', ownerFeatures: ['product-full'] }, - { depName: 'git2', ownerFeatures: ['service-integrations'] }, - { depName: 'glob', ownerFeatures: ['product-full'] }, - { depName: 'globset', ownerFeatures: ['product-full'] }, - { depName: 'image', ownerFeatures: ['service-integrations', 'tool-packs'] }, + { depName: 'image', ownerFeatures: ['product-full', 'tool-packs'] }, { depName: 'include_dir', ownerFeatures: ['product-full'] }, { depName: 'indexmap', ownerFeatures: ['product-full'] }, - { depName: 'md5', ownerFeatures: ['product-full', 'service-integrations'] }, - { depName: 'rand', ownerFeatures: ['service-integrations'] }, - { depName: 'reqwest', ownerFeatures: ['ai-adapter-runtime', 'service-integrations'] }, - { depName: 'rmcp', ownerFeatures: ['service-integrations'] }, - { depName: 'russh', ownerFeatures: ['ssh-remote'] }, + { depName: 'md5', ownerFeatures: ['product-full'] }, + { depName: 'reqwest', ownerFeatures: ['ai-adapter-runtime', 'product-full'] }, + { depName: 'rmcp', ownerFeatures: ['product-full'] }, { depName: 'similar', ownerFeatures: ['product-full'] }, - { depName: 'sse-stream', ownerFeatures: ['service-integrations'] }, - { depName: 'tokio-tungstenite', ownerFeatures: ['service-integrations'] }, - { depName: 'tower-http', ownerFeatures: ['service-integrations'] }, + { depName: 'tokio-tungstenite', ownerFeatures: ['product-full'] }, + { depName: 'tower-http', ownerFeatures: ['product-full'] }, { depName: 'tool-runtime', ownerFeatures: ['product-full'] }, ], }, @@ -142,15 +138,66 @@ export const coreProductFullFeatureAssemblyRule = { manifestPath: 'src/crates/assembly/core/Cargo.toml', featureName: 'product-full', requiredFeatureRefs: [ + 'announcement', + 'file-watch', + 'git', + 'review-platform', 'ssh-remote', 'product-capabilities', 'product-domains', - 'service-integrations', 'tool-packs', ], reason: 'bitfun-core product-full must explicitly assemble current owner feature groups', }; +export const coreClosedFeatureProfileRules = [ + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'announcement', + requiredFeatureRefs: ['bitfun-services-integrations/announcement'], + exact: true, + reason: 'bitfun-core announcement must select only the announcement owner capability', + }, + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'file-watch', + requiredFeatureRefs: ['bitfun-services-integrations/file-watch'], + exact: true, + reason: 'bitfun-core file-watch must select only the file-watch owner capability', + }, + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'git', + requiredFeatureRefs: ['bitfun-services-integrations/git'], + exact: true, + reason: 'bitfun-core git must select only the Git owner capability', + }, + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'review-platform', + requiredFeatureRefs: ['bitfun-services-integrations/review-platform'], + exact: true, + reason: + 'bitfun-core review-platform must select only the review platform owner capability', + }, + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'service-integrations', + requiredFeatureRefs: ['announcement', 'file-watch', 'git', 'review-platform'], + exact: true, + reason: + 'bitfun-core service-integrations is a compatibility facade group, not a product capability umbrella', + }, + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'ssh-remote', + requiredFeatureRefs: ['bitfun-services-integrations/remote-ssh-concrete'], + exact: true, + reason: + 'bitfun-core ssh-remote must select only the concrete SSH capability and must not pull product Dispatch assembly', + }, +]; + export const ownerCrateFeatureAssemblyRules = [ { manifestPath: 'src/crates/execution/tool-provider-groups/Cargo.toml', diff --git a/scripts/core-boundaries/rules/source/required-rules.mjs b/scripts/core-boundaries/rules/source/required-rules.mjs index eadde1c5c5..6f7f8de415 100644 --- a/scripts/core-boundaries/rules/source/required-rules.mjs +++ b/scripts/core-boundaries/rules/source/required-rules.mjs @@ -3892,8 +3892,8 @@ export const requiredContentRules = [ message: 'MiniApp product domain facade must stay behind product-domains', }, { - regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub\(crate\) mod service_agent_runtime\b/s, - message: 'service agent runtime owner assembly must stay behind service-integrations', + regex: /#\[cfg\(feature = "product-full"\)\]\s*pub\(crate\) mod service_agent_runtime\b/s, + message: 'service agent runtime owner assembly must stay behind product-full', }, ], }, @@ -3902,6 +3902,11 @@ export const requiredContentRules = [ reason: 'no-default dispatch cleanup must retain claimed records when the product worktree owner is unavailable', patterns: [ + { + regex: + /#\[cfg\(feature = "product-full"\)\]\s*mod baseline;[\s\S]*?#\[cfg\(feature = "product-full"\)\]\s*mod controller;[\s\S]*?#\[cfg\(feature = "product-full"\)\]\s*mod device_controller;[\s\S]*?#\[cfg\(feature = "product-full"\)\]\s*mod preparation;/s, + message: 'Dispatch product controllers must stay behind product-full', + }, { regex: /#\[cfg\(feature = "product-full"\)\]\s*async fn release_baseline_claim\b/s, @@ -3952,20 +3957,28 @@ export const requiredContentRules = [ 'service integration and agent-runtime surfaces must not compile in no-default core builds', patterns: [ { - regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub mod git\b/s, - message: 'git service facade must stay behind service-integrations', + regex: /#\[cfg\(feature = "announcement"\)\]\s*pub mod announcement\b/s, + message: 'announcement facade must stay behind its exact feature', + }, + { + regex: /#\[cfg\(feature = "file-watch"\)\]\s*pub use bitfun_services_integrations::file_watch\b/s, + message: 'file-watch facade must stay behind its exact feature', + }, + { + regex: /#\[cfg\(feature = "git"\)\]\s*pub mod git\b/s, + message: 'git service facade must stay behind its exact feature', }, { - regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub mod mcp\b/s, - message: 'MCP service facade must stay behind service-integrations', + regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod mcp\b/s, + message: 'Core MCP product bridge must stay behind product-full', }, { - regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub mod remote_connect\b/s, - message: 'remote-connect service facade must stay behind service-integrations', + regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod remote_connect\b/s, + message: 'Core Remote Connect product bridge must stay behind product-full', }, { - regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub mod review_platform\b/s, - message: 'review platform facade must stay behind service-integrations', + regex: /#\[cfg\(feature = "review-platform"\)\]\s*pub mod review_platform\b/s, + message: 'review platform facade must stay behind its exact feature', }, { regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod search\b/s, @@ -3999,11 +4012,11 @@ export const requiredContentRules = [ patterns: [ { regex: - /#\[cfg\(feature = "service-integrations"\)\]\s*use super::worktree_topology::global_worktree_topology_service\b/s, + /#\[cfg\(feature = "git"\)\]\s*use super::worktree_topology::global_worktree_topology_service\b/s, message: 'worktree topology owner import must stay gated for no-default builds', }, { - regex: /#\[cfg\(not\(feature = "service-integrations"\)\)\]\s*\{\s*let _ = \(workspace_root, freshness\);\s*return None;\s*\}/s, + regex: /#\[cfg\(not\(feature = "git"\)\)\]\s*\{\s*let _ = \(workspace_root, freshness\);\s*return None;\s*\}/s, message: 'no-default worktree enrichment fallback must remain explicit', }, ], diff --git a/scripts/core-boundaries/self-test.mjs b/scripts/core-boundaries/self-test.mjs index 32b953edc1..d32b4d97f7 100644 --- a/scripts/core-boundaries/self-test.mjs +++ b/scripts/core-boundaries/self-test.mjs @@ -7,6 +7,7 @@ export function runManifestParserSelfTest({ parseManifestDependencies, manifestDependencyMatches, matchingForbiddenDependency, + coreClosedFeatureProfileRules, coreProductFullFeatureAssemblyRule, ownerCrateFeatureAssemblyRules, parseManifestFeatures, @@ -144,16 +145,45 @@ export function runManifestParserSelfTest({ } for (const featureName of [ + 'announcement', + 'file-watch', + 'git', + 'review-platform', 'ssh-remote', 'product-capabilities', 'product-domains', - 'service-integrations', 'tool-packs', ]) { if (!coreProductFullFeatureAssemblyRule.requiredFeatureRefs.includes(featureName)) { throw new Error(`core product-full assembly rule must require ${featureName}`); } } + const closedCoreProfiles = new Map( + coreClosedFeatureProfileRules.map((rule) => [rule.featureName, rule]), + ); + const expectedClosedCoreProfiles = new Map([ + ['announcement', ['bitfun-services-integrations/announcement']], + ['file-watch', ['bitfun-services-integrations/file-watch']], + ['git', ['bitfun-services-integrations/git']], + ['review-platform', ['bitfun-services-integrations/review-platform']], + ['service-integrations', ['announcement', 'file-watch', 'git', 'review-platform']], + ['ssh-remote', ['bitfun-services-integrations/remote-ssh-concrete']], + ]); + for (const [featureName, expectedReferences] of expectedClosedCoreProfiles) { + const rule = closedCoreProfiles.get(featureName); + if (!rule?.exact) { + throw new Error(`core closed feature profile must cover ${featureName} exactly`); + } + if ( + rule.requiredFeatureRefs.length !== expectedReferences.length + || expectedReferences.some((reference) => !rule.requiredFeatureRefs.includes(reference)) + ) { + throw new Error(`core closed feature profile has stale references for ${featureName}`); + } + if (rule.requiredFeatureRefs.some((reference) => reference.includes('product-full'))) { + throw new Error(`core closed feature profile must not hide product-full in ${featureName}`); + } + } const ownerFeatureRulePaths = new Set( ownerCrateFeatureAssemblyRules.map((rule) => rule.manifestPath), ); @@ -183,12 +213,18 @@ export function runManifestParserSelfTest({ 'default = ["product-full"]', 'product-full = [', ' "dep:tool-runtime",', - ' "service-integrations",', + ' "announcement",', + ' "file-watch",', + ' "git",', + ' "review-platform",', ']', - 'service-integrations = ["dep:git2", "dep:rmcp"]', + 'announcement = ["bitfun-services-integrations/announcement"]', + 'file-watch = ["bitfun-services-integrations/file-watch"]', + 'git = ["bitfun-services-integrations/git"]', + 'review-platform = ["bitfun-services-integrations/review-platform"]', + 'service-integrations = ["announcement", "file-watch", "git", "review-platform"]', 'ssh-remote = [', ' "bitfun-services-integrations/remote-ssh-concrete",', - ' "russh",', ']', '[dependencies]', 'git2 = { workspace = true, optional = true }', @@ -199,11 +235,15 @@ export function runManifestParserSelfTest({ if (!parsedFeatures.get('product-full')?.refs.includes('dep:tool-runtime')) { throw new Error('feature parser must detect multiline dependency feature references'); } - if (!parsedFeatures.get('service-integrations')?.refs.includes('dep:rmcp')) { + if (!parsedFeatures.get('service-integrations')?.refs.includes('git')) { throw new Error('feature parser must detect inline dependency feature references'); } - if (!parsedFeatures.get('ssh-remote')?.refs.includes('russh')) { - throw new Error('feature parser must detect implicit optional dependency feature references'); + if ( + !parsedFeatures + .get('ssh-remote') + ?.refs.includes('bitfun-services-integrations/remote-ssh-concrete') + ) { + throw new Error('feature parser must detect dependency capability feature references'); } const acceptsGitFacadeLine = createFacadeLineChecker('bitfun_services_integrations::git'); @@ -550,6 +590,10 @@ export function runManifestParserSelfTest({ 'rmcp', 'image', 'tool-runtime', + 'rustls', + 'rustls-native-certs', + 'schannel', + 'win32job', 'bitfun-relay-service', 'htmd', 'legible', @@ -567,14 +611,26 @@ export function runManifestParserSelfTest({ ); const coreFullyMigratedDeps = new Set([ 'aes', + 'aes-gcm', 'bitfun-relay-service', + 'eventsource-stream', + 'git2', + 'glob', + 'globset', 'hostname', 'htmd', 'legible', 'local-ip-address', 'mac_address', 'qrcode', + 'rand', 'readability-js', + 'russh', + 'rustls', + 'rustls-native-certs', + 'schannel', + 'sse-stream', + 'win32job', 'x25519-dalek', ]); for (const dep of coreProfile?.forbiddenNonOptionalDeps ?? []) { @@ -585,17 +641,11 @@ export function runManifestParserSelfTest({ throw new Error(`core optional dependency owner rule must cover forbidden dependency ${dep}`); } } - for (const dep of ['git2', 'rmcp', 'image', 'tool-runtime']) { + for (const dep of ['rmcp', 'image', 'tool-runtime']) { if (!coreOptionalOwnerDeps.has(dep)) { throw new Error(`core optional dependency owner rule must cover ${dep}`); } } - const coreGit2Owner = coreOptionalOwnerRule?.dependencies.find( - (dependency) => dependency.depName === 'git2', - ); - if (!coreGit2Owner?.ownerFeatures.includes('service-integrations')) { - throw new Error('core optional dependency owner rule must keep git2 under service-integrations'); - } const servicesOptionalOwnerRule = optionalDependencyFeatureOwnerRules.find( (rule) => rule.crateName === 'services-integrations', ); @@ -3789,7 +3839,7 @@ export function runManifestParserSelfTest({ 'feature = "product-domains"', 'pub mod function_agents', 'pub mod miniapp', - 'feature = "service-integrations"', + 'feature = "product-full"', 'service_agent_runtime', ], }, @@ -3815,12 +3865,17 @@ export function runManifestParserSelfTest({ { path: 'src/crates/assembly/core/src/service/mod.rs', contracts: [ - 'feature = "service-integrations"', + 'feature = "announcement"', + 'pub mod announcement', + 'feature = "file-watch"', + 'file_watch', + 'feature = "git"', 'pub mod git', + 'feature = "product-full"', 'pub mod mcp', 'pub mod remote_connect', + 'feature = "review-platform"', 'pub mod review_platform', - 'feature = "product-full"', 'pub mod search', 'pub mod snapshot', ], @@ -3832,7 +3887,7 @@ export function runManifestParserSelfTest({ { path: 'src/crates/assembly/core/src/service/workspace/manager.rs', contracts: [ - 'feature = "service-integrations"', + 'feature = "git"', 'global_worktree_topology_service', 'return None', ], diff --git a/src/crates/assembly/core/AGENTS.md b/src/crates/assembly/core/AGENTS.md index e0a5fa2486..a5298c4a55 100644 --- a/src/crates/assembly/core/AGENTS.md +++ b/src/crates/assembly/core/AGENTS.md @@ -79,6 +79,12 @@ SessionManager -> Session -> DialogTurn -> ModelRound - Feature work must keep `product-full` as the compatibility product assembly boundary unless a separate product matrix review changes default capability selection. +- Keep the light compatibility features independently compilable: + `announcement`, `file-watch`, `git`, and `review-platform` own their matching + service facade, while `service-integrations` is only their compatibility + aggregate. `ssh-remote` selects the concrete SSH service only; Dispatch + controllers, MCP, Remote Connect, and agent runtime wiring remain + `product-full` product composition. - Keep `cargo check -p bitfun-core --no-default-features` viable. Gate product-only modules at their owner feature; if a light facade operation cannot safely complete without a product owner, fail closed and preserve any diff --git a/src/crates/assembly/core/Cargo.toml b/src/crates/assembly/core/Cargo.toml index 0644fa2eec..161512c351 100644 --- a/src/crates/assembly/core/Cargo.toml +++ b/src/crates/assembly/core/Cargo.toml @@ -45,7 +45,6 @@ reqwest = { workspace = true, optional = true } axum = { workspace = true, optional = true } tower-http = { workspace = true, optional = true } -glob = { workspace = true, optional = true } notify = { workspace = true } dirs = { workspace = true } dunce = { workspace = true } @@ -54,33 +53,25 @@ fs2 = { workspace = true, optional = true } flate2 = { workspace = true, optional = true } include_dir = { workspace = true, optional = true } -git2 = { workspace = true, optional = true } - # Command detection (cross-platform) similar = { workspace = true, optional = true } urlencoding = { workspace = true } -globset = { workspace = true, optional = true } - -eventsource-stream = { workspace = true, optional = true } - # MCP Streamable HTTP client (official rust-sdk) rmcp = { workspace = true, features = [ "transport-streamable-http-client-reqwest", ], optional = true } -sse-stream = { workspace = true, optional = true } - # Shared AI protocol adapters bitfun-ai-adapters = { path = "../../adapters/ai-adapters", optional = true } # Lightweight agent stream processing -bitfun-agent-stream = { path = "../../execution/agent-stream" } +bitfun-agent-stream = { path = "../../execution/agent-stream", optional = true } # Agent runtime owner contracts -bitfun-agent-runtime = { path = "../../execution/agent-runtime" } +bitfun-agent-runtime = { path = "../../execution/agent-runtime", optional = true } # Harness workflow contracts -bitfun-harness = { path = "../../execution/harness" } +bitfun-harness = { path = "../../execution/harness", optional = true } # Product capability pack contracts bitfun-product-capabilities = { path = "../product-capabilities", default-features = false, optional = true } @@ -122,24 +113,18 @@ terminal-core = { path = "../../services/terminal" } fluent-bundle = { workspace = true } unic-langid = { workspace = true } -# Encryption (Remote Connect E2E) -aes-gcm = { workspace = true, optional = true } sha2 = { workspace = true } -rand = { workspace = true, optional = true } # QR code generation # WebSocket client tokio-tungstenite = { workspace = true, optional = true } -# SSH - Remote SSH support (optional feature) -russh = { workspace = true, optional = true } - # Event layer dependency (lowest layer) bitfun-core-types = { path = "../../contracts/core-types" } bitfun-events = { path = "../../contracts/events" } bitfun-runtime-ports = { path = "../../contracts/runtime-ports", features = ["permission"] } -bitfun-runtime-services = { path = "../../execution/runtime-services" } +bitfun-runtime-services = { path = "../../execution/runtime-services", optional = true } # Reviewed product-full plugin composition root. bitfun-opencode-adapter = { path = "../../adapters/opencode-adapter", optional = true } @@ -150,16 +135,6 @@ bitfun-plugin-runtime-client = { path = "../../execution/plugin-runtime-client", # Transport layer dependency bitfun-transport = { path = "../../adapters/transport" } -# Non-Windows: vendored OpenSSL for libgit2 (no system install). -[target.'cfg(not(windows))'.dependencies] -git2 = { workspace = true, features = ["vendored-openssl"], optional = true } - -[target.'cfg(windows)'.dependencies] -win32job = { workspace = true } -rustls = { workspace = true } -rustls-native-certs = "0.8" -schannel = "0.1" - [features] # Full product runtime feature set. Product crates should depend on this # explicitly before `bitfun-core` default features are made lighter. @@ -167,15 +142,16 @@ default = ["product-full"] product-full = [ "ai-adapter-runtime", "canvas-runtime", + "dep:axum", + "dep:bitfun-agent-runtime", + "dep:bitfun-agent-stream", + "dep:bitfun-harness", "dep:chrono-tz", "dep:cron", "dep:dashmap", - "dep:eventsource-stream", "dep:filetime", "dep:flate2", "dep:fs2", - "dep:glob", - "dep:globset", "dep:include_dir", "dep:bitfun-opencode-adapter", "dep:bitfun-claude-code-adapter", @@ -183,14 +159,23 @@ product-full = [ "dep:bitfun-external-sources", "dep:bitfun-plugin-runtime-client", "dep:indexmap", + "dep:image", "dep:md5", + "dep:reqwest", + "dep:rmcp", "dep:similar", + "dep:tokio-tungstenite", + "dep:tower-http", "dep:tool-runtime", + "bitfun-services-integrations/product-full", "ssh-remote", "product-capabilities", "product-domains", "runtime-services", - "service-integrations", + "announcement", + "file-watch", + "git", + "review-platform", "tool-packs", ] ai-adapter-runtime = [ @@ -210,33 +195,24 @@ product-domains = [ "bitfun-services-integrations/miniapp-runtime", "bitfun-services-integrations/miniapp-market", "bitfun-product-domains/product-full", + "runtime-services", ] canvas-runtime = [ "product-domains", "bitfun-services-integrations/canvas-runtime", ] -runtime-services = [] -service-integrations = [ - "dep:aes-gcm", - "dep:axum", - "dep:git2", - "dep:image", - "dep:md5", - "dep:rand", - "dep:reqwest", - "dep:rmcp", - "dep:sse-stream", - "dep:tokio-tungstenite", - "dep:tower-http", - "bitfun-services-integrations/product-full", -] +runtime-services = ["dep:bitfun-runtime-services"] +announcement = ["bitfun-services-integrations/announcement"] +file-watch = ["bitfun-services-integrations/file-watch"] +git = ["bitfun-services-integrations/git"] +review-platform = ["bitfun-services-integrations/review-platform"] +service-integrations = ["announcement", "file-watch", "git", "review-platform"] tool-packs = ["dep:bitfun-tool-packs", "bitfun-tool-packs/product-full", "dep:image"] # Deprecated compatibility feature. Tauri integration is desktop-owned now; # keep the public feature name as a no-op for downstream manifests. tauri-support = [] ssh-remote = [ "bitfun-services-integrations/remote-ssh-concrete", - "russh", ] [dev-dependencies] @@ -249,7 +225,7 @@ required-features = ["product-full"] [[test]] name = "git_contracts" -required-features = ["service-integrations"] +required-features = ["git"] [[test]] name = "product_assembly" @@ -257,11 +233,11 @@ required-features = ["product-full"] [[test]] name = "remote_mcp_streamable_http" -required-features = ["service-integrations"] +required-features = ["product-full"] [[test]] name = "remote_connect_host_boundary" -required-features = ["service-integrations"] +required-features = ["product-full"] [build-dependencies] sha2 = { workspace = true } diff --git a/src/crates/assembly/core/src/external_hooks.rs b/src/crates/assembly/core/src/external_hooks.rs index 67b8877899..d13569ee2d 100644 --- a/src/crates/assembly/core/src/external_hooks.rs +++ b/src/crates/assembly/core/src/external_hooks.rs @@ -12,7 +12,7 @@ pub use bitfun_product_domains::external_hook_catalog::{ pub use bitfun_product_domains::external_sources::{ExecutionDomainId, ExternalSourceContext}; use crate::external_sources::{host_execution_domain_id, normalize_workspace_root}; -#[cfg(feature = "service-integrations")] +#[cfg(feature = "git")] use crate::service::workspace::{global_worktree_topology_service, WorktreeTopologyFreshness}; use bitfun_claude_code_adapter::{ClaudeCodeHookProvider, ClaudeCodeHookProviderOptions}; use bitfun_codex_adapter::{CodexHookProvider, CodexHookProviderOptions}; @@ -297,7 +297,7 @@ pub(crate) struct HookProjectTopology { pub(crate) primary_root: Option, } -#[cfg(feature = "service-integrations")] +#[cfg(feature = "git")] async fn hook_project_topology( workspace_root: Option<&std::path::Path>, ) -> Option { @@ -315,7 +315,7 @@ async fn hook_project_topology( ) } -#[cfg(not(feature = "service-integrations"))] +#[cfg(not(feature = "git"))] async fn hook_project_topology( _workspace_root: Option<&std::path::Path>, ) -> Option { diff --git a/src/crates/assembly/core/src/infrastructure/events/mod.rs b/src/crates/assembly/core/src/infrastructure/events/mod.rs index 5f5d1715ee..15d868255a 100644 --- a/src/crates/assembly/core/src/infrastructure/events/mod.rs +++ b/src/crates/assembly/core/src/infrastructure/events/mod.rs @@ -1,11 +1,14 @@ //! Event system module pub mod emitter; +#[cfg(feature = "runtime-services")] pub mod event_system; pub use bitfun_transport::TransportEmitter; pub use emitter::EventEmitter; +#[cfg(feature = "runtime-services")] pub use event_system::BackendEventSystem as BackendEventManager; +#[cfg(feature = "runtime-services")] pub use event_system::{ emit_global_event, get_global_event_system, BackendEvent, BackendEventSystem, }; diff --git a/src/crates/assembly/core/src/infrastructure/mod.rs b/src/crates/assembly/core/src/infrastructure/mod.rs index f941d7f4b0..03cf23e915 100644 --- a/src/crates/assembly/core/src/infrastructure/mod.rs +++ b/src/crates/assembly/core/src/infrastructure/mod.rs @@ -16,6 +16,7 @@ pub mod subscription_auth; #[cfg(feature = "ai-adapter-runtime")] pub use ai::AIClient; pub use app_paths::{get_path_manager_arc, try_get_path_manager_arc, PathManager, StorageLevel}; +#[cfg(feature = "runtime-services")] pub use events::BackendEventManager; pub use filesystem::{ BatchedFileSearchProgressSink, FileContentSearchOptions, FileInfo, FileNameSearchOptions, diff --git a/src/crates/assembly/core/src/lib.rs b/src/crates/assembly/core/src/lib.rs index a93fe70c94..748d845fe7 100644 --- a/src/crates/assembly/core/src/lib.rs +++ b/src/crates/assembly/core/src/lib.rs @@ -51,7 +51,7 @@ pub mod runtime_ownership; #[cfg(all(test, feature = "product-full"))] mod runtime_ownership_tests; pub mod service; // Workspace, Config, FileSystem, Terminal, Git -#[cfg(feature = "service-integrations")] +#[cfg(feature = "product-full")] pub(crate) mod service_agent_runtime; pub mod util; // General types, errors, helper functions @@ -73,6 +73,7 @@ pub use service::{ // Export infrastructure components #[cfg(feature = "ai-adapter-runtime")] pub use infrastructure::ai::AIClient; +#[cfg(feature = "runtime-services")] pub use infrastructure::events::BackendEventManager; // Export Agentic service core types diff --git a/src/crates/assembly/core/src/product_runtime/runtime_services.rs b/src/crates/assembly/core/src/product_runtime/runtime_services.rs index 94f72e9f16..7e4b66fdd2 100644 --- a/src/crates/assembly/core/src/product_runtime/runtime_services.rs +++ b/src/crates/assembly/core/src/product_runtime/runtime_services.rs @@ -20,7 +20,6 @@ use terminal_core::TerminalRuntimePort; use crate::agentic::session::CoreSessionStorePort; -#[cfg(feature = "service-integrations")] use crate::service_agent_runtime::{ CoreRemoteWorkspaceFileRuntimeHost, CoreRemoteWorkspaceRuntimeHost, }; @@ -90,22 +89,14 @@ impl RuntimeServicesProvider for CoreRuntimeServicesProvider { #[cfg(feature = "ssh-remote")] let builder = builder.with_optional_remote_exec(Some(Self::remote_exec_port())); - #[cfg(feature = "service-integrations")] - { - let remote_workspace: Arc = - Arc::new(CoreRemoteWorkspaceRuntimeHost::new()); - let remote_projection: Arc = - Arc::new(CoreRemoteWorkspaceFileRuntimeHost::new()); + let remote_workspace: Arc = + Arc::new(CoreRemoteWorkspaceRuntimeHost::new()); + let remote_projection: Arc = + Arc::new(CoreRemoteWorkspaceFileRuntimeHost::new()); - builder - .with_optional_remote_workspace(Some(remote_workspace)) - .with_optional_remote_projection(Some(remote_projection)) - } - - #[cfg(not(feature = "service-integrations"))] - { - builder - } + builder + .with_optional_remote_workspace(Some(remote_workspace)) + .with_optional_remote_projection(Some(remote_projection)) } } @@ -130,7 +121,7 @@ impl RuntimeServicesProvider for CoreLocalRuntimeServicesProvider { .with_events(self.ports.events()) .with_clock(self.ports.clock()); - #[cfg(feature = "service-integrations")] + #[cfg(feature = "git")] let builder = builder.with_optional_git(Some(Arc::new( bitfun_services_integrations::git::GitWorkspaceDiffPort::new( self.ports.workspace_root(), @@ -192,7 +183,11 @@ mod local_runtime_tests { #[tokio::test] async fn local_runtime_services_bind_git_queries_to_the_canonical_workspace() { let workspace = tempfile::tempdir().expect("workspace"); - git2::Repository::init(workspace.path()).expect("git repository"); + bitfun_services_integrations::git::execute_git_command_sync( + workspace.path().to_string_lossy().as_ref(), + &["init"], + ) + .expect("git repository"); std::fs::write(workspace.path().join("new.txt"), "new file\n").expect("workspace file"); let (_, services) = diff --git a/src/crates/assembly/core/src/service/announcement/remote.rs b/src/crates/assembly/core/src/service/announcement/remote.rs index 0b64087805..1dc0c0d9fd 100644 --- a/src/crates/assembly/core/src/service/announcement/remote.rs +++ b/src/crates/assembly/core/src/service/announcement/remote.rs @@ -62,16 +62,12 @@ impl RemoteFetcher { async fn current_locale() -> String { use crate::service::config::get_global_config_service; - get_global_config_service() + let Ok(service) = get_global_config_service().await else { + return "en-US".to_string(); + }; + service + .get_config::(Some("general.language")) .await - .ok() - .and_then(|svc| { - tokio::task::block_in_place(|| { - tokio::runtime::Handle::current() - .block_on(svc.get_config::(Some("general.language"))) - .ok() - }) - }) - .unwrap_or_else(|| "en-US".to_string()) + .unwrap_or_else(|_| "en-US".to_string()) } } diff --git a/src/crates/assembly/core/src/service/config/service.rs b/src/crates/assembly/core/src/service/config/service.rs index 1d340248fa..37218c4c8d 100644 --- a/src/crates/assembly/core/src/service/config/service.rs +++ b/src/crates/assembly/core/src/service/config/service.rs @@ -120,7 +120,7 @@ impl ConfigService { /// Atomically replaces one JSON configuration value when its current value /// still matches the caller's snapshot. The read, comparison, and persisted /// write share the existing manager write lock. - #[cfg(any(test, feature = "service-integrations"))] + #[cfg(any(test, feature = "product-full"))] pub(crate) async fn compare_and_set_json_config( &self, path: &str, diff --git a/src/crates/assembly/core/src/service/dispatch/controller.rs b/src/crates/assembly/core/src/service/dispatch/controller.rs index caed475394..7bcb25b839 100644 --- a/src/crates/assembly/core/src/service/dispatch/controller.rs +++ b/src/crates/assembly/core/src/service/dispatch/controller.rs @@ -163,7 +163,7 @@ pub struct DispatchAppendRequest { /// The wire shape and structural limits come from the shared contract; the /// controller only adds transport-owned policy (the device inline budget). -pub use bitfun_services_core::dispatch_contract::DispatchAttachment as DispatchAttachmentPayload; +pub(super) use bitfun_services_core::dispatch_contract::DispatchAttachment as DispatchAttachmentPayload; pub(super) fn validate_attachment_payloads( attachments: &[DispatchAttachmentPayload], @@ -179,8 +179,7 @@ pub(super) fn validate_device_attachment_budget( .iter() .map(|attachment| attachment.data_url.len()) .sum(); - if total - > bitfun_services_core::dispatch_contract::MAX_DEVICE_DISPATCH_ATTACHMENTS_TOTAL_BYTES + if total > bitfun_services_core::dispatch_contract::MAX_DEVICE_DISPATCH_ATTACHMENTS_TOTAL_BYTES { anyhow::bail!( "Device dispatch carries at most 192 KiB of inline images; use an SSH target for larger screenshots" @@ -1078,8 +1077,7 @@ pub(super) fn continue_payload(request: &DispatchContinueRequest) -> Value { payload["kind"] = Value::String(kind.to_string()); } if !request.attachments.is_empty() { - payload["attachments"] = serde_json::to_value(&request.attachments) - .unwrap_or(Value::Null); + payload["attachments"] = serde_json::to_value(&request.attachments).unwrap_or(Value::Null); } payload } diff --git a/src/crates/assembly/core/src/service/dispatch/mod.rs b/src/crates/assembly/core/src/service/dispatch/mod.rs index fe8be98d2e..43062911e2 100644 --- a/src/crates/assembly/core/src/service/dispatch/mod.rs +++ b/src/crates/assembly/core/src/service/dispatch/mod.rs @@ -1,10 +1,10 @@ -#[cfg(feature = "ssh-remote")] +#[cfg(feature = "product-full")] mod baseline; -#[cfg(feature = "ssh-remote")] +#[cfg(feature = "product-full")] mod controller; -#[cfg(feature = "ssh-remote")] +#[cfg(feature = "product-full")] mod device_controller; -#[cfg(feature = "ssh-remote")] +#[cfg(feature = "product-full")] mod preparation; mod target; @@ -20,7 +20,7 @@ use tokio::fs; use crate::infrastructure::PathManager; -#[cfg(feature = "ssh-remote")] +#[cfg(feature = "product-full")] pub use controller::{ answer as answer_dispatch, append as append_dispatch, cancel as cancel_dispatch, continue_job as continue_dispatch_job, install_cli_cancel as cancel_dispatch_cli_install, @@ -33,17 +33,16 @@ pub use controller::{ DispatchContinueRequest, DispatchInstallPollRequest, DispatchInstallStartRequest, DispatchJobRequest, DispatchListJobsRequest, DispatchListTargetsRequest, DispatchPermissionReplyKind, DispatchProbeTargetRequest, DispatchQueryJobRequest, - DispatchStatusRequest, - DispatchSubmitRequest, DispatchSyncResultRequest, DispatchTargetOption, + DispatchStatusRequest, DispatchSubmitRequest, DispatchSyncResultRequest, DispatchTargetOption, }; -#[cfg(feature = "ssh-remote")] +#[cfg(feature = "product-full")] pub use device_controller::{ answer_device as answer_device_dispatch, append_device as append_device_dispatch, cancel_device as cancel_device_dispatch, continue_device_job as continue_device_dispatch_job, list_device_jobs as list_device_dispatch_jobs, probe_device as probe_device_dispatch_target, - query_device_job as query_device_dispatch_job, - status_device as get_device_dispatch_status, submit_device as submit_device_dispatch, - sync_device_result as sync_device_dispatch_result, DeviceDispatchRpc, + query_device_job as query_device_dispatch_job, status_device as get_device_dispatch_status, + submit_device as submit_device_dispatch, sync_device_result as sync_device_dispatch_result, + DeviceDispatchRpc, }; pub use target::{DispatchTarget, DispatchTargetRequest, DispatchWorkspaceDelivery}; @@ -52,7 +51,7 @@ const PROMPT_PREVIEW_CHARS: usize = 160; /// controller's baseline worktree. pub(super) const OUTBOUND_RESULTS_DIR: &str = ".results"; /// Where base bundles are built before being uploaded to a target. -#[cfg(feature = "ssh-remote")] +#[cfg(feature = "product-full")] const OUTBOUND_BUNDLES_DIR: &str = ".bundles"; /// Where the renderer's observer transcript cache lives. const OUTBOUND_TRANSCRIPTS_DIR: &str = ".transcripts"; @@ -66,7 +65,7 @@ const MAX_OUTBOUND_TRANSCRIPT_BYTES: usize = 8 * 1024 * 1024; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[cfg(feature = "ssh-remote")] +#[cfg(feature = "product-full")] struct DispatchTargetJobEntry { job_id: String, session_id: String, @@ -383,7 +382,7 @@ impl OutboundDispatchStore { } pub async fn list(&self) -> Result, DispatchStoreError> { - #[cfg(feature = "ssh-remote")] + #[cfg(feature = "product-full")] if let Err(error) = self.reconcile_expired_preparations().await { log::warn!("Failed to reconcile expired dispatch preparations: {error}"); } @@ -522,7 +521,7 @@ impl OutboundDispatchStore { /// /// Bundles hold repository contents, so they get the same private treatment /// as everything else the controller writes here. - #[cfg(feature = "ssh-remote")] + #[cfg(feature = "product-full")] pub(crate) async fn bundles_dir(&self) -> anyhow::Result { let bundles = self.root.join(OUTBOUND_BUNDLES_DIR); fs::create_dir_all(&bundles).await?; @@ -531,7 +530,7 @@ impl OutboundDispatchStore { } /// Owner-only staging directory for bundles fetched back from a target. - #[cfg(feature = "ssh-remote")] + #[cfg(feature = "product-full")] pub(crate) async fn results_dir(&self) -> anyhow::Result { let results = self.root.join(OUTBOUND_RESULTS_DIR); fs::create_dir_all(&results).await?; @@ -701,7 +700,7 @@ async fn remove_file_if_present(path: &Path) -> anyhow::Result<()> { } } -#[cfg(feature = "ssh-remote")] +#[cfg(feature = "product-full")] async fn adopt_target_jobs( store: &OutboundDispatchStore, target: &DispatchTarget, @@ -796,7 +795,7 @@ async fn adopt_target_jobs( /// Validate a path returned by the target without applying the controller /// process's host path semantics. The target may run POSIX while the controller /// runs Windows, or vice versa. -#[cfg(feature = "ssh-remote")] +#[cfg(feature = "product-full")] fn target_workspace_path_is_absolute(path: &str) -> bool { let path = path.trim(); if path.starts_with('/') { @@ -821,7 +820,7 @@ fn target_workspace_path_is_absolute(path: &str) -> bool { components.next().is_some() && components.next().is_some() } -#[cfg(feature = "ssh-remote")] +#[cfg(feature = "product-full")] fn same_target_identity_for_store(left: &DispatchTarget, right: &DispatchTarget) -> bool { match (left, right) { ( @@ -1403,7 +1402,7 @@ mod tests { assert_eq!(record.prompt_preview.chars().count(), PROMPT_PREVIEW_CHARS); } - #[cfg(feature = "ssh-remote")] + #[cfg(feature = "product-full")] #[test] fn target_workspace_paths_use_target_platform_semantics() { assert!(target_workspace_path_is_absolute("/srv/app")); @@ -1418,7 +1417,7 @@ mod tests { assert!(!target_workspace_path_is_absolute(r"\\server")); } - #[cfg(feature = "ssh-remote")] + #[cfg(feature = "product-full")] #[tokio::test] async fn listing_a_target_adopts_observer_records_without_runtime_ownership() { let temp = tempfile::tempdir().expect("tempdir"); diff --git a/src/crates/assembly/core/src/service/mod.rs b/src/crates/assembly/core/src/service/mod.rs index 66fb2202fa..038fd46898 100644 --- a/src/crates/assembly/core/src/service/mod.rs +++ b/src/crates/assembly/core/src/service/mod.rs @@ -4,7 +4,7 @@ //! isolated. High-coupling runtime services stay here until their port //! contracts and equivalence tests are explicit. -#[cfg(feature = "service-integrations")] +#[cfg(feature = "announcement")] pub mod announcement; // Announcement / feature-demo / tips system pub(crate) mod bootstrap; // Workspace persona bootstrap helpers #[cfg(feature = "canvas-runtime")] @@ -14,18 +14,18 @@ pub mod config; // Config management pub mod cron; // Scheduled jobs pub mod dispatch; // Outbound dispatch observer index and target contracts pub mod filesystem; // FileSystem management -#[cfg(feature = "service-integrations")] +#[cfg(feature = "git")] pub mod git; // Git service pub mod i18n; // I18n service #[cfg(feature = "product-full")] pub(crate) mod instruction_context; // Workspace instruction file prompt helpers pub mod lsp; // LSP (Language Server Protocol) system -#[cfg(feature = "service-integrations")] +#[cfg(feature = "product-full")] pub mod mcp; // MCP (Model Context Protocol) system -#[cfg(feature = "service-integrations")] +#[cfg(feature = "product-full")] pub mod remote_connect; // Remote Connect (phone → desktop) pub mod remote_ssh; // Remote SSH (desktop → server) -#[cfg(feature = "service-integrations")] +#[cfg(feature = "review-platform")] pub mod review_platform; // Pull request review platform adapters pub mod runtime; // Managed runtime and capability management #[cfg(feature = "product-full")] @@ -47,10 +47,10 @@ pub mod worktree; // Managed Git worktree lifecycle and session bindings pub use terminal_core as terminal; // Re-export main components. -#[cfg(feature = "service-integrations")] +#[cfg(feature = "announcement")] pub use announcement::{AnnouncementCard, AnnouncementScheduler, AnnouncementSchedulerRef}; pub use bitfun_services_core::{diagnostics, diff, system}; -#[cfg(feature = "service-integrations")] +#[cfg(feature = "file-watch")] pub use bitfun_services_integrations::file_watch; pub use bootstrap::reset_workspace_persona_files_to_default; #[cfg(feature = "canvas-runtime")] @@ -63,20 +63,20 @@ pub use cron::{ pub use diff::{ DiffConfig, DiffHunk, DiffLine, DiffLineType, DiffOptions, DiffResult, DiffService, }; -#[cfg(feature = "service-integrations")] +#[cfg(feature = "file-watch")] pub use file_watch::{ get_global_file_watch_service, get_watched_paths, initialize_file_watch_service, start_file_watch, stop_file_watch, FileWatchEvent, FileWatchEventKind, FileWatchService, FileWatcherConfig, }; pub use filesystem::{DirectoryStats, FileSystemService, FileSystemServiceFactory}; -#[cfg(feature = "service-integrations")] +#[cfg(feature = "git")] pub use git::GitService; pub use i18n::{get_global_i18n_service, I18nConfig, I18nService, LocaleId, LocaleMetadata}; pub use lsp::LspManager; -#[cfg(feature = "service-integrations")] +#[cfg(feature = "product-full")] pub use mcp::MCPService; -#[cfg(feature = "service-integrations")] +#[cfg(feature = "review-platform")] pub use review_platform::{ ReviewAuthSource, ReviewAuthState, ReviewChecks, ReviewDecision, ReviewEvidenceCompleteness, ReviewFileStatus, ReviewItemState, ReviewPlatformAccount, ReviewPlatformAuthChallenge, diff --git a/src/crates/assembly/core/src/service/workspace/manager.rs b/src/crates/assembly/core/src/service/workspace/manager.rs index 66b58241ee..58cecdecc9 100644 --- a/src/crates/assembly/core/src/service/workspace/manager.rs +++ b/src/crates/assembly/core/src/service/workspace/manager.rs @@ -1,6 +1,6 @@ //! Workspace manager. -#[cfg(feature = "service-integrations")] +#[cfg(feature = "git")] use super::worktree_topology::global_worktree_topology_service; use super::WorktreeTopologyFreshness; use crate::service::remote_ssh::workspace_state::{ @@ -450,13 +450,13 @@ impl WorkspaceInfo { workspace_root: &Path, freshness: WorktreeTopologyFreshness, ) -> Option { - #[cfg(not(feature = "service-integrations"))] + #[cfg(not(feature = "git"))] { let _ = (workspace_root, freshness); return None; } - #[cfg(feature = "service-integrations")] + #[cfg(feature = "git")] { let normalized_workspace_path = workspace_root.to_string_lossy().replace('\\', "/"); let worktrees = match global_worktree_topology_service() diff --git a/src/crates/assembly/core/src/service/workspace/mod.rs b/src/crates/assembly/core/src/service/workspace/mod.rs index 1d5a63f417..aeb4b6cfda 100644 --- a/src/crates/assembly/core/src/service/workspace/mod.rs +++ b/src/crates/assembly/core/src/service/workspace/mod.rs @@ -7,7 +7,7 @@ pub mod identity_watch; pub mod manager; pub mod provider; pub mod service; -#[cfg(feature = "service-integrations")] +#[cfg(feature = "git")] pub mod worktree_topology; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -31,5 +31,5 @@ pub use service::{ WorkspaceHealthStatus, WorkspaceIdentityChangedEvent, WorkspaceImportResult, WorkspaceInfoUpdates, WorkspaceQuickSummary, WorkspaceService, }; -#[cfg(feature = "service-integrations")] +#[cfg(feature = "git")] pub use worktree_topology::{global_worktree_topology_service, WorktreeTopologyService}; diff --git a/src/crates/assembly/core/src/service/workspace/service.rs b/src/crates/assembly/core/src/service/workspace/service.rs index 7483187bab..e335cb7f92 100644 --- a/src/crates/assembly/core/src/service/workspace/service.rs +++ b/src/crates/assembly/core/src/service/workspace/service.rs @@ -13,7 +13,7 @@ use crate::infrastructure::{try_get_path_manager_arc, PathManager}; use crate::service::bootstrap::{ ensure_workspace_gitignore_ignores_bitfun, initialize_workspace_persona_files, }; -#[cfg(feature = "service-integrations")] +#[cfg(feature = "git")] use crate::service::git::{GitError, GitWorktreeInfo}; use crate::service::remote_ssh::workspace_state::{ canonicalize_local_workspace_root, get_remote_workspace_manager, init_remote_workspace_manager, @@ -617,7 +617,7 @@ impl WorkspaceService { result } - #[cfg(feature = "service-integrations")] + #[cfg(feature = "git")] pub async fn list_worktrees( &self, path: &Path, @@ -628,7 +628,7 @@ impl WorkspaceService { .await } - #[cfg(feature = "service-integrations")] + #[cfg(feature = "git")] pub async fn is_live_worktree_root_in_same_repository( &self, registered_path: &Path, @@ -639,7 +639,7 @@ impl WorkspaceService { .await } - #[cfg(feature = "service-integrations")] + #[cfg(feature = "git")] pub async fn invalidate_worktree_topology(&self, path: &Path) { super::worktree_topology::global_worktree_topology_service() .invalidate(path) diff --git a/src/crates/assembly/core/src/util/errors.rs b/src/crates/assembly/core/src/util/errors.rs index 28061e383c..2828799b9e 100644 --- a/src/crates/assembly/core/src/util/errors.rs +++ b/src/crates/assembly/core/src/util/errors.rs @@ -206,6 +206,7 @@ impl BitFunError { } } +#[cfg(feature = "product-full")] impl From for BitFunError { fn from(error: bitfun_agent_stream::StreamProcessorError) -> Self { match error { @@ -216,6 +217,7 @@ impl From for BitFunError { } } +#[cfg(feature = "product-full")] impl From for BitFunError { fn from(error: bitfun_agent_runtime::event_bus::EventBusError) -> Self { Self::Agent(error.to_string()) @@ -228,7 +230,7 @@ impl From for BitFun } } -#[cfg(feature = "service-integrations")] +#[cfg(feature = "product-full")] impl From for BitFunError { fn from(error: bitfun_services_integrations::mcp::MCPRuntimeError) -> Self { use bitfun_services_integrations::mcp::MCPRuntimeErrorKind; diff --git a/src/crates/assembly/core/tests/remote_connect_host_boundary.rs b/src/crates/assembly/core/tests/remote_connect_host_boundary.rs index 938d0baf3e..9e07736359 100644 --- a/src/crates/assembly/core/tests/remote_connect_host_boundary.rs +++ b/src/crates/assembly/core/tests/remote_connect_host_boundary.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "service-integrations")] +#![cfg(feature = "product-full")] use bitfun_core::service::remote_connect::embedded_relay_host::EmbeddedRelayHost; use bitfun_core::service::remote_connect::{