import fs from 'node:fs/promises'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const RUST_CRATES_ROOT = path.join(REPO_ROOT, 'src-tauri', 'crates'); const HOST_CRATE_ROOT = path.join(RUST_CRATES_ROOT, 'tauritavern'); const HOST_SRC_ROOT = path.join(HOST_CRATE_ROOT, 'src'); const WORKSPACE_MANIFEST = path.join(REPO_ROOT, 'src-tauri', 'Cargo.toml'); const DEPENDENCY_TREE_LABEL = 'all-features/all-targets'; const DEPENDENCY_TREE_ARGS = ['--all-features', '--target', 'all', '-e', 'normal,build,dev']; const DOMAIN_FORBIDDEN_PACKAGES = new Set([ 'async-trait', 'axum', 'image', 'miktik', 'reqwest', 'rmcp', 'tar', 'tauri', 'tauritavern', 'tokio', 'ttsync-core', 'zip', ]); const DOMAIN_FORBIDDEN_SOURCE_PATTERNS = [ ['main crate domain path', /\bcrate::domain::/], ['repository path', /\bcrate::repositories::/], ['async-trait', /\basync_trait\b/], ['axum', /\baxum::/], ['image', /\bimage::/], ['miktik', /\bmiktik::/], ['reqwest', /\breqwest::/], ['rmcp', /\brmcp::/], ['tauri', /\btauri::/], ['tokio', /\btokio::/], ['filesystem IO', /\bstd::fs::/], ['network IO', /\bstd::net::/], ]; const CONTRACTS_FORBIDDEN_PACKAGES = new Set([ 'async-trait', 'axum', 'image', 'miktik', 'reqwest', 'rmcp', 'tar', 'tauri', 'tauritavern', 'tokio', 'tt-ports', 'ttsync-core', 'zip', ]); const CONTRACTS_FORBIDDEN_SOURCE_PATTERNS = [ ['async-trait', /\basync_trait\b/], ['axum', /\baxum::/], ['image', /\bimage::/], ['miktik', /\bmiktik::/], ['reqwest', /\breqwest::/], ['rmcp', /\brmcp::/], ['tauri', /\btauri::/], ['tokio', /\btokio::/], ['tt-ports', /\btt_ports::/], ['ttsync-core', /\bttsync_core::/], ['filesystem IO', /\bstd::fs::/], ['network IO', /\bstd::net::/], ]; const PORTS_FORBIDDEN_PACKAGES = new Set([ 'axum', 'image', 'miktik', 'reqwest', 'rmcp', 'tar', 'tauri', 'tauritavern', 'ttsync-core', 'zip', ]); const PORTS_FORBIDDEN_SOURCE_PATTERNS = [ ['axum', /\baxum::/], ['image', /\bimage::/], ['miktik', /\bmiktik::/], ['reqwest', /\breqwest::/], ['rmcp', /\brmcp::/], ['tauri', /\btauri::/], ['ttsync-core', /\bttsync_core::/], ['filesystem IO', /\bstd::fs::/], ['network IO', /\bstd::net::/], ]; const APPLICATION_FORBIDDEN_PACKAGES = new Set([ 'async-compression', 'axum', 'image', 'miktik', 'qrcode', 'reqwest', 'rmcp', 'tar', 'tauri', 'tauritavern', 'tokio-tungstenite', 'tt-adapter-archive', 'tt-adapter-extension', 'tt-adapter-http', 'tt-adapter-media', 'tt-adapter-mcp', 'tt-adapter-provider-http', 'tt-adapter-quickjs', 'tt-adapter-storage-core', 'tt-adapter-storage-userdata', 'tt-adapter-sync', 'tt-adapter-tokenization', 'tt-adapter-vector', 'tt-adapter-triviumdb', 'yup-oauth2', 'zip', ]); const APPLICATION_FORBIDDEN_SOURCE_PATTERNS = [ ['domain facade path', /\bcrate::domain::/], ['app host path', /\bcrate::app::/], ['infrastructure path', /\bcrate::infrastructure::/], ['presentation path', /\bcrate::presentation::/], ['platform path', /\bcrate::platform::/], ['axum', /\baxum::/], ['image', /\bimage::/], ['miktik', /\bmiktik::/], ['qrcode', /\bqrcode::/], ['reqwest', /\breqwest::/], ['rmcp', /\brmcp::/], ['tauri', /\btauri::/], ['tar', /\btar::/], ['tt-adapter-archive', /\btt_adapter_archive::/], ['tt-adapter-extension', /\btt_adapter_extension::/], ['tt-adapter-http', /\btt_adapter_http::/], ['tt-adapter-media', /\btt_adapter_media::/], ['tt-adapter-mcp', /\btt_adapter_mcp::/], ['tt-adapter-provider-http', /\btt_adapter_provider_http::/], ['tt-adapter-quickjs', /\btt_adapter_quickjs::/], ['tt-adapter-storage-core', /\btt_adapter_storage_core::/], ['tt-adapter-storage-userdata', /\btt_adapter_storage_userdata::/], ['tt-adapter-sync', /\btt_adapter_sync::/], ['tt-adapter-tokenization', /\btt_adapter_tokenization::/], ['tt-adapter-vector', /\btt_adapter_vector::/], ['tt-adapter-triviumdb', /\btt_adapter_triviumdb::/], ['main crate', /\btauritavern(_lib)?::/], ['provider oauth client', /\byup_oauth2::/], ['concrete hyper client', /\bhyper_util::/], ['concrete rustls client', /\brustls::/], ['tls root store', /\bwebpki_roots::/], ['tokio-tungstenite', /\btokio_tungstenite::/], ['sys-locale', /\bsys_locale::/], ['icu collator', /\bicu_collator::/], ['icu locale', /\bicu_locale_core::/], ['zip', /\bzip::/], ]; const ADAPTER_HTTP_FORBIDDEN_PACKAGES = new Set([ 'axum', 'image', 'miktik', 'tar', 'tauri', 'tauritavern', 'tt-adapter-extension', 'tt-adapter-storage-userdata', 'tt-application', 'ttsync-core', 'zip', ]); const ADAPTER_FORBIDDEN_SOURCE_PATTERNS = [ ['application path', /\bcrate::application::/], ['app host path', /\bcrate::app::/], ['infrastructure path', /\bcrate::infrastructure::/], ['presentation path', /\bcrate::presentation::/], ['tauri', /\btauri::/], ['tt-adapter-extension', /\btt_adapter_extension::/], ['tt-application', /\btt_application::/], ['main crate', /\btauritavern(_lib)?::/], ]; const ADAPTER_TOKENIZATION_FORBIDDEN_PACKAGES = new Set([ 'axum', 'image', 'tar', 'tauri', 'tauritavern', 'tt-adapter-extension', 'tt-adapter-storage-userdata', 'tt-application', 'ttsync-core', 'zip', ]); const ADAPTER_TOKENIZATION_FORBIDDEN_SOURCE_PATTERNS = [ ...ADAPTER_FORBIDDEN_SOURCE_PATTERNS, ['axum', /\baxum::/], ['image', /\bimage::/], ['reqwest', /\breqwest::/], ['tt-adapter-storage-userdata', /\btt_adapter_storage_userdata::/], ['ttsync-core', /\bttsync_core::/], ['zip', /\bzip::/], ]; const ADAPTER_SYNC_FORBIDDEN_PACKAGES = new Set([ 'image', 'miktik', 'qrcode', 'tar', 'tauri', 'tauritavern', 'tt-adapter-extension', 'tt-adapter-http', 'tt-adapter-storage-userdata', 'tt-adapter-tokenization', 'tt-application', 'zip', ]); const ADAPTER_SYNC_FORBIDDEN_SOURCE_PATTERNS = [ ...ADAPTER_FORBIDDEN_SOURCE_PATTERNS, ['image', /\bimage::/], ['qrcode', /\bqrcode::/], ['tt-adapter-http', /\btt_adapter_http::/], ['tt-adapter-storage-userdata', /\btt_adapter_storage_userdata::/], ['tt-adapter-tokenization', /\btt_adapter_tokenization::/], ['zip', /\bzip::/], ]; const ADAPTER_ARCHIVE_FORBIDDEN_PACKAGES = new Set([ 'axum', 'image', 'miktik', 'qrcode', 'reqwest', 'tauri', 'tauritavern', 'tt-adapter-http', 'tt-adapter-extension', 'tt-adapter-storage-userdata', 'tt-adapter-tokenization', 'tt-adapter-sync', 'tt-application', 'ttsync-core', ]); const ADAPTER_ARCHIVE_FORBIDDEN_SOURCE_PATTERNS = [ ...ADAPTER_FORBIDDEN_SOURCE_PATTERNS, ['axum', /\baxum::/], ['image', /\bimage::/], ['miktik', /\bmiktik::/], ['qrcode', /\bqrcode::/], ['reqwest', /\breqwest::/], ['tt-adapter-http', /\btt_adapter_http::/], ['tt-adapter-storage-userdata', /\btt_adapter_storage_userdata::/], ['tt-adapter-tokenization', /\btt_adapter_tokenization::/], ['tt-adapter-sync', /\btt_adapter_sync::/], ['ttsync-core', /\bttsync_core::/], ]; const ADAPTER_PROVIDER_HTTP_FORBIDDEN_PACKAGES = new Set([ 'axum', 'image', 'miktik', 'qrcode', 'tar', 'tauri', 'tauritavern', 'tt-adapter-archive', 'tt-adapter-extension', 'tt-adapter-sync', 'tt-adapter-storage-userdata', 'tt-adapter-tokenization', 'tt-application', 'ttsync-core', 'zip', ]); const ADAPTER_PROVIDER_HTTP_FORBIDDEN_SOURCE_PATTERNS = [ ...ADAPTER_FORBIDDEN_SOURCE_PATTERNS, ['axum', /\baxum::/], ['image', /\bimage::/], ['miktik', /\bmiktik::/], ['qrcode', /\bqrcode::/], ['tt-adapter-archive', /\btt_adapter_archive::/], ['tt-adapter-sync', /\btt_adapter_sync::/], ['tt-adapter-storage-userdata', /\btt_adapter_storage_userdata::/], ['tt-adapter-tokenization', /\btt_adapter_tokenization::/], ['ttsync-core', /\bttsync_core::/], ['zip', /\bzip::/], ]; const ADAPTER_VECTOR_FORBIDDEN_PACKAGES = new Set([ 'axum', 'miktik', 'qrcode', 'reqwest', 'rmcp', 'tar', 'tauri', 'tauritavern', 'tt-adapter-archive', 'tt-adapter-extension', 'tt-adapter-http', 'tt-adapter-media', 'tt-adapter-mcp', 'tt-adapter-provider-http', 'tt-adapter-storage-core', 'tt-adapter-storage-userdata', 'tt-adapter-sync', 'tt-adapter-tokenization', 'tt-application', 'ttsync-core', ]); const ADAPTER_VECTOR_FORBIDDEN_SOURCE_PATTERNS = [ ...ADAPTER_FORBIDDEN_SOURCE_PATTERNS, ['axum', /\baxum::/], ['image', /\bimage::/], ['miktik', /\bmiktik::/], ['qrcode', /\bqrcode::/], ['reqwest', /\breqwest::/], ['rmcp', /\brmcp::/], ['tar', /\btar::/], ['tt-adapter-http', /\btt_adapter_http::/], ['tt-adapter-media', /\btt_adapter_media::/], ['tt-adapter-mcp', /\btt_adapter_mcp::/], ['tt-adapter-provider-http', /\btt_adapter_provider_http::/], ['tt-adapter-storage-core', /\btt_adapter_storage_core::/], ['tt-adapter-storage-userdata', /\btt_adapter_storage_userdata::/], ['tt-adapter-sync', /\btt_adapter_sync::/], ['tt-adapter-tokenization', /\btt_adapter_tokenization::/], ['ttsync-core', /\bttsync_core::/], ['zip', /\bzip::/], ]; const ADAPTER_MCP_FORBIDDEN_PACKAGES = new Set([ 'async-compression', 'axum', 'image', 'miktik', 'qrcode', 'tar', 'tauri', 'tauritavern', 'tt-adapter-archive', 'tt-adapter-extension', 'tt-adapter-media', 'tt-adapter-provider-http', 'tt-adapter-storage-core', 'tt-adapter-storage-userdata', 'tt-adapter-sync', 'tt-adapter-tokenization', 'tt-application', 'ttsync-core', 'zip', ]); const ADAPTER_MCP_FORBIDDEN_SOURCE_PATTERNS = [ ...ADAPTER_FORBIDDEN_SOURCE_PATTERNS, ['axum', /\baxum::/], ['image', /\bimage::/], ['miktik', /\bmiktik::/], ['qrcode', /\bqrcode::/], ['tar', /\btar::/], ['tauri', /\btauri::/], ['tt-adapter-storage-core', /\btt_adapter_storage_core::/], ['tt-adapter-storage-userdata', /\btt_adapter_storage_userdata::/], ['tt-adapter-sync', /\btt_adapter_sync::/], ['ttsync-core', /\bttsync_core::/], ['zip', /\bzip::/], ]; const ADAPTER_STORAGE_CORE_FORBIDDEN_PACKAGES = new Set([ 'axum', 'image', 'mime_guess', 'miktik', 'qrcode', 'reqwest', 'tar', 'tauri', 'tauritavern', 'tt-adapter-archive', 'tt-adapter-extension', 'tt-adapter-http', 'tt-adapter-provider-http', 'tt-adapter-storage-userdata', 'tt-adapter-sync', 'tt-adapter-tokenization', 'tt-application', 'ttsync-core', 'zip', ]); const ADAPTER_STORAGE_CORE_FORBIDDEN_SOURCE_PATTERNS = [ ...ADAPTER_FORBIDDEN_SOURCE_PATTERNS, ['axum', /\baxum::/], ['image', /\bimage::/], ['mime_guess', /\bmime_guess::/], ['miktik', /\bmiktik::/], ['qrcode', /\bqrcode::/], ['reqwest', /\breqwest::/], ['tar', /\btar::/], ['tt-adapter-archive', /\btt_adapter_archive::/], ['tt-adapter-http', /\btt_adapter_http::/], ['tt-adapter-provider-http', /\btt_adapter_provider_http::/], ['tt-adapter-storage-userdata', /\btt_adapter_storage_userdata::/], ['tt-adapter-sync', /\btt_adapter_sync::/], ['tt-adapter-tokenization', /\btt_adapter_tokenization::/], ['ttsync-core', /\bttsync_core::/], ['zip', /\bzip::/], ['network IO', /\bstd::net::/], ]; const ADAPTER_STORAGE_USERDATA_FORBIDDEN_PACKAGES = new Set([ 'axum', 'miktik', 'qrcode', 'reqwest', 'tar', 'tauri', 'tauritavern', 'tt-adapter-archive', 'tt-adapter-extension', 'tt-adapter-http', 'tt-adapter-media', 'tt-adapter-provider-http', 'tt-adapter-sync', 'tt-adapter-tokenization', 'tt-application', 'ttsync-core', ]); const ADAPTER_STORAGE_USERDATA_FORBIDDEN_SOURCE_PATTERNS = [ ...ADAPTER_FORBIDDEN_SOURCE_PATTERNS, ['axum', /\baxum::/], ['miktik', /\bmiktik::/], ['qrcode', /\bqrcode::/], ['reqwest', /\breqwest::/], ['tar', /\btar::/], ['tt-adapter-archive', /\btt_adapter_archive::/], ['tt-adapter-http', /\btt_adapter_http::/], ['tt-adapter-media', /\btt_adapter_media::/], ['tt-adapter-provider-http', /\btt_adapter_provider_http::/], ['tt-adapter-sync', /\btt_adapter_sync::/], ['tt-adapter-tokenization', /\btt_adapter_tokenization::/], ['ttsync-core', /\bttsync_core::/], ['network IO', /\bstd::net::/], ]; const ADAPTER_MEDIA_FORBIDDEN_PACKAGES = new Set([ 'axum', 'miktik', 'qrcode', 'reqwest', 'tar', 'tauri', 'tauritavern', 'tt-adapter-archive', 'tt-adapter-extension', 'tt-adapter-http', 'tt-adapter-provider-http', 'tt-adapter-sync', 'tt-adapter-storage-userdata', 'tt-adapter-tokenization', 'tt-application', 'ttsync-core', 'zip', ]); const ADAPTER_MEDIA_FORBIDDEN_SOURCE_PATTERNS = [ ...ADAPTER_FORBIDDEN_SOURCE_PATTERNS, ['axum', /\baxum::/], ['miktik', /\bmiktik::/], ['qrcode', /\bqrcode::/], ['reqwest', /\breqwest::/], ['tar', /\btar::/], ['tt-adapter-archive', /\btt_adapter_archive::/], ['tt-adapter-http', /\btt_adapter_http::/], ['tt-adapter-provider-http', /\btt_adapter_provider_http::/], ['tt-adapter-sync', /\btt_adapter_sync::/], ['tt-adapter-storage-userdata', /\btt_adapter_storage_userdata::/], ['tt-adapter-tokenization', /\btt_adapter_tokenization::/], ['ttsync-core', /\bttsync_core::/], ['zip', /\bzip::/], ['network IO', /\bstd::net::/], ]; const ADAPTER_QUICKJS_FORBIDDEN_PACKAGES = new Set([ 'axum', 'miktik', 'qrcode', 'reqwest', 'rmcp', 'tar', 'tauri', 'tauritavern', 'tt-adapter-archive', 'tt-adapter-extension', 'tt-adapter-http', 'tt-adapter-media', 'tt-adapter-mcp', 'tt-adapter-provider-http', 'tt-adapter-storage-core', 'tt-adapter-storage-userdata', 'tt-adapter-sync', 'tt-adapter-tokenization', 'tt-adapter-vector', 'tt-adapter-triviumdb', 'tt-application', 'ttsync-core', ]); const ADAPTER_QUICKJS_FORBIDDEN_SOURCE_PATTERNS = [ ...ADAPTER_FORBIDDEN_SOURCE_PATTERNS, ['axum', /\baxum::/], ['image', /\bimage::/], ['miktik', /\bmiktik::/], ['qrcode', /\bqrcode::/], ['reqwest', /\breqwest::/], ['rmcp', /\brmcp::/], ['tar', /\btar::/], ['tt-adapter-archive', /\btt_adapter_archive::/], ['tt-adapter-http', /\btt_adapter_http::/], ['tt-adapter-media', /\btt_adapter_media::/], ['tt-adapter-mcp', /\btt_adapter_mcp::/], ['tt-adapter-provider-http', /\btt_adapter_provider_http::/], ['tt-adapter-storage-core', /\btt_adapter_storage_core::/], ['tt-adapter-storage-userdata', /\btt_adapter_storage_userdata::/], ['tt-adapter-sync', /\btt_adapter_sync::/], ['tt-adapter-tokenization', /\btt_adapter_tokenization::/], ['tt-adapter-vector', /\btt_adapter_vector::/], ['tt-adapter-triviumdb', /\btt_adapter_triviumdb::/], ['ttsync-core', /\bttsync_core::/], ['zip', /\bzip::/], ]; const ADAPTER_EXTENSION_FORBIDDEN_PACKAGES = new Set([ 'axum', 'image', 'miktik', 'qrcode', 'tar', 'tauri', 'tauritavern', 'tt-adapter-archive', 'tt-adapter-media', 'tt-adapter-provider-http', 'tt-adapter-storage-userdata', 'tt-adapter-sync', 'tt-adapter-tokenization', 'tt-application', 'ttsync-core', ]); const ADAPTER_EXTENSION_FORBIDDEN_SOURCE_PATTERNS = [ ...ADAPTER_FORBIDDEN_SOURCE_PATTERNS, ['axum', /\baxum::/], ['image', /\bimage::/], ['miktik', /\bmiktik::/], ['qrcode', /\bqrcode::/], ['tar', /\btar::/], ['tt-adapter-archive', /\btt_adapter_archive::/], ['tt-adapter-media', /\btt_adapter_media::/], ['tt-adapter-provider-http', /\btt_adapter_provider_http::/], ['tt-adapter-storage-userdata', /\btt_adapter_storage_userdata::/], ['tt-adapter-sync', /\btt_adapter_sync::/], ['tt-adapter-tokenization', /\btt_adapter_tokenization::/], ['ttsync-core', /\bttsync_core::/], ]; const CRATES = [ crateConfig('tt-domain', DOMAIN_FORBIDDEN_PACKAGES, DOMAIN_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-contracts', CONTRACTS_FORBIDDEN_PACKAGES, CONTRACTS_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-ports', PORTS_FORBIDDEN_PACKAGES, PORTS_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-application', APPLICATION_FORBIDDEN_PACKAGES, APPLICATION_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-adapter-http', ADAPTER_HTTP_FORBIDDEN_PACKAGES, ADAPTER_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-adapter-mcp', ADAPTER_MCP_FORBIDDEN_PACKAGES, ADAPTER_MCP_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-adapter-tokenization', ADAPTER_TOKENIZATION_FORBIDDEN_PACKAGES, ADAPTER_TOKENIZATION_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-adapter-sync', ADAPTER_SYNC_FORBIDDEN_PACKAGES, ADAPTER_SYNC_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-adapter-archive', ADAPTER_ARCHIVE_FORBIDDEN_PACKAGES, ADAPTER_ARCHIVE_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-adapter-provider-http', ADAPTER_PROVIDER_HTTP_FORBIDDEN_PACKAGES, ADAPTER_PROVIDER_HTTP_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-adapter-vector', ADAPTER_VECTOR_FORBIDDEN_PACKAGES, ADAPTER_VECTOR_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-adapter-triviumdb', new Set(['tauri', 'tauritavern', 'tt-application']), ADAPTER_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-adapter-extension', ADAPTER_EXTENSION_FORBIDDEN_PACKAGES, ADAPTER_EXTENSION_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-adapter-storage-core', ADAPTER_STORAGE_CORE_FORBIDDEN_PACKAGES, ADAPTER_STORAGE_CORE_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-adapter-storage-userdata', ADAPTER_STORAGE_USERDATA_FORBIDDEN_PACKAGES, ADAPTER_STORAGE_USERDATA_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-adapter-media', ADAPTER_MEDIA_FORBIDDEN_PACKAGES, ADAPTER_MEDIA_FORBIDDEN_SOURCE_PATTERNS), crateConfig('tt-adapter-quickjs', ADAPTER_QUICKJS_FORBIDDEN_PACKAGES, ADAPTER_QUICKJS_FORBIDDEN_SOURCE_PATTERNS), ]; const MAIN_CRATE_SOURCE_RULES = [ sourceRule('main crate removed layer shims', HOST_SRC_ROOT, [ ['domain module declaration', /^\s*(pub\s+)?mod\s+domain\s*;/], ['domain shim path', /\bcrate::domain::/], ['application module declaration', /^\s*(pub\s+)?mod\s+application\s*;/], ['application shim path', /\bcrate::application::/], ]), sourceRule('infrastructure', path.join(HOST_SRC_ROOT, 'infrastructure'), [ ['tt-application crate', /\btt_application::/], ]), sourceRule('platform', path.join(HOST_SRC_ROOT, 'platform'), [ ['tt-application crate', /\btt_application::/], ]), sourceRule('app composition', path.join(HOST_SRC_ROOT, 'app', 'composition'), [ ['repository facade', /\bcrate::domain::repositories::/], ['sync contract facade', /\bcrate::domain::models::sync(_automation)?::/], ]), sourceRule('application provider auth boundary', path.join(REPO_ROOT, 'src-tauri', 'crates', 'tt-application', 'src'), [ ['provider oauth client', /\byup_oauth2::/], ['concrete hyper client', /\bhyper_util::/], ['concrete rustls client', /\brustls::/], ['tls root store', /\bwebpki_roots::/], ]), ]; function crateConfig(name, forbiddenPackages, forbiddenSourcePatterns) { const root = path.join(RUST_CRATES_ROOT, name); return { name, root, src: path.join(root, 'src'), manifest: path.join(root, 'Cargo.toml'), forbiddenPackages, forbiddenSourcePatterns, }; } function sourceRule(name, root, forbiddenSourcePatterns) { return { name, root, forbiddenSourcePatterns, }; } function toPosixPath(value) { return String(value).replace(/\\/g, '/'); } function loadWorkspaceMetadata() { const result = spawnSync('cargo', [ 'metadata', '--manifest-path', WORKSPACE_MANIFEST, '--all-features', '--format-version', '1', ], { cwd: REPO_ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, }); if (result.status !== 0) { throw new Error(`cargo metadata failed:\n${result.stderr || result.stdout}`); } return JSON.parse(result.stdout); } function indexWorkspaceMetadata(metadata) { const packages = new Map(metadata.packages.map((entry) => [entry.id, entry])); const nodes = new Map(metadata.resolve.nodes.map((entry) => [entry.id, entry])); const workspacePackages = new Map(metadata.workspace_members.map((id) => [packages.get(id)?.name, id])); return { packages, nodes, workspacePackages }; } async function listFiles(dir) { const entries = await fs.readdir(dir, { withFileTypes: true }); const files = []; for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { files.push(...await listFiles(fullPath)); continue; } if (entry.isFile()) { files.push(fullPath); } } return files; } async function fileExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } async function crateFiles(config) { const files = [ ...(await listFiles(config.src)).filter((file) => file.endsWith('.rs')), config.manifest, ]; const buildScript = path.join(config.root, 'build.rs'); if (await fileExists(buildScript)) { files.push(buildScript); } return files; } async function sourceRuleFiles(config) { return (await listFiles(config.root)).filter((file) => file.endsWith('.rs')); } async function checkSourceBoundaries(config) { const violations = []; for (const filePath of await crateFiles(config)) { const relPath = toPosixPath(path.relative(REPO_ROOT, filePath)); const text = await fs.readFile(filePath, 'utf8'); const lines = text.split(/\r?\n/); lines.forEach((line, index) => { for (const [kind, pattern] of config.forbiddenSourcePatterns) { if (pattern.test(line)) { violations.push(`${relPath}:${index + 1}: ${config.name} ${kind}: ${line.trim()}`); } } }); } return violations; } async function checkMainCrateSourceRule(config) { const violations = []; for (const filePath of await sourceRuleFiles(config)) { const relPath = toPosixPath(path.relative(REPO_ROOT, filePath)); const text = await fs.readFile(filePath, 'utf8'); const lines = text.split(/\r?\n/); lines.forEach((line, index) => { for (const [kind, pattern] of config.forbiddenSourcePatterns) { if (pattern.test(line)) { violations.push(`${relPath}:${index + 1}: ${config.name} ${kind}: ${line.trim()}`); } } }); } return violations; } function checkDependencyTree(config) { const violations = []; const result = spawnSync('cargo', [ 'tree', '--manifest-path', WORKSPACE_MANIFEST, '-p', config.name, ...DEPENDENCY_TREE_ARGS, '--prefix', 'none', ], { cwd: REPO_ROOT, encoding: 'utf8', }); if (result.status !== 0) { return [`${config.name} cargo tree (${DEPENDENCY_TREE_LABEL}) failed:\n${result.stderr || result.stdout}`]; } const packages = new Set( result.stdout .split(/\r?\n/) .map((line) => line.trim().split(/\s+/)[0]) .filter(Boolean), ); for (const name of config.forbiddenPackages) { if (packages.has(name)) { violations.push(`${config.name} ${DEPENDENCY_TREE_LABEL} dependency tree includes forbidden package: ${name}`); } } return violations; } // The all-features workspace graph is an upper bound. Skip the exact per-crate tree only when // Cargo exposed the root completely and no forbidden package is reachable from it. function metadataMayContainForbiddenPackage(config, metadata) { const rootId = metadata.workspacePackages.get(config.name); const rootPackage = metadata.packages.get(rootId); const rootNode = metadata.nodes.get(rootId); if (!rootPackage || !rootNode) { return true; } const activeFeatures = new Set(rootNode.features); if (Object.keys(rootPackage.features).some((feature) => !activeFeatures.has(feature))) { return true; } const directPackages = new Set(rootNode.deps.map((dependency) => metadata.packages.get(dependency.pkg)?.name)); if (rootPackage.dependencies.some((dependency) => !directPackages.has(dependency.name))) { return true; } const seen = new Set(); const pending = [rootId]; while (pending.length > 0) { const id = pending.pop(); if (seen.has(id)) { continue; } seen.add(id); if (config.forbiddenPackages.has(metadata.packages.get(id)?.name)) { return true; } pending.push(...(metadata.nodes.get(id)?.dependencies ?? [])); } return false; } function checkDirectDependencies(config, metadata) { const rustPackage = metadata.packages.get(metadata.workspacePackages.get(config.name)); if (!rustPackage) { return [`${config.name} package missing from cargo metadata`]; } return rustPackage.dependencies .filter((dependency) => config.forbiddenPackages.has(dependency.name)) .map((dependency) => { const qualifiers = [ dependency.kind, dependency.optional ? 'optional' : null, dependency.target, ].filter(Boolean); const suffix = qualifiers.length > 0 ? ` (${qualifiers.join(', ')})` : ''; return `${config.name} Cargo.toml declares forbidden dependency: ${dependency.name}${suffix}`; }); } async function main() { const metadata = indexWorkspaceMetadata(loadWorkspaceMetadata()); const violations = []; for (const config of CRATES) { violations.push( ...checkDirectDependencies(config, metadata), ...(await checkSourceBoundaries(config)), ...(metadataMayContainForbiddenPackage(config, metadata) ? checkDependencyTree(config) : []), ); } for (const config of MAIN_CRATE_SOURCE_RULES) { violations.push(...await checkMainCrateSourceRule(config)); } if (violations.length > 0) { console.error(`[rust-crate-boundaries] FAILED\n${violations.join('\n')}`); process.exitCode = 1; return; } console.log(`[rust-crate-boundaries] clean (${CRATES.map((config) => config.name).join(', ')})`); } main().catch((error) => { console.error('[rust-crate-boundaries] failed:', error); process.exitCode = 1; });