Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions scripts/check-core-boundaries.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
},
],
);
});
61 changes: 61 additions & 0 deletions scripts/core-boundaries/checker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
cratePathForName,
} from './rules/crate-layout.mjs';
import {
coreClosedFeatureProfileRules,
coreProductFullFeatureAssemblyRule,
optionalDependencyFeatureOwnerRules,
ownerCrateFeatureAssemblyRules,
Expand All @@ -32,6 +33,7 @@ import {
featureReferencesDependency,
featureReferencesFeature,
unexpectedDependencyOwnerFeatures,
unexpectedReachableLocalFeatures,
} from './manifest-feature-helpers.mjs';
import { checkCargoDependencyBoundariesSafely } from './cargo-dependency-boundaries.mjs';

Expand Down Expand Up @@ -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/));
Expand Down Expand Up @@ -1004,6 +1061,7 @@ export function runCoreBoundaryCheck() {
parseManifestDependencies,
manifestDependencyMatches,
matchingForbiddenDependency,
coreClosedFeatureProfileRules,
coreProductFullFeatureAssemblyRule,
ownerCrateFeatureAssemblyRules,
parseManifestFeatures,
Expand Down Expand Up @@ -1064,6 +1122,9 @@ export function runCoreBoundaryCheck() {

checkCoreDefaultProductFullFeature();
checkCoreProductFullFeatureAssembly(coreProductFullFeatureAssemblyRule);
for (const rule of coreClosedFeatureProfileRules) {
checkClosedFeatureProfile(rule);
}
for (const rule of ownerCrateFeatureAssemblyRules) {
checkOwnerCrateFeatureAssembly(rule);
}
Expand Down
31 changes: 31 additions & 0 deletions scripts/core-boundaries/manifest-feature-helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 4 additions & 0 deletions scripts/core-boundaries/rules/crate-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
},
Expand Down
79 changes: 63 additions & 16 deletions scripts/core-boundaries/rules/feature-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'] },
],
},
Expand Down Expand Up @@ -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',
Expand Down
Loading
Loading