/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { execFileSync } from 'node:child_process'; import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { builtinModules } from 'node:module'; import { tmpdir } from 'node:os'; import { dirname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { parse } from '@babel/parser'; const STATEFUL_HOOKS = new Set([ 'use', 'useActionState', 'useDeferredValue', 'useEffect', 'useImperativeHandle', 'useInsertionEffect', 'useLayoutEffect', 'useOptimistic', 'useReducer', 'useRef', 'useState', 'useSyncExternalStore', 'useTransition', ]); const REACT_LIFECYCLE_METHODS = new Set([ 'UNSAFE_componentWillMount', 'UNSAFE_componentWillReceiveProps', 'UNSAFE_componentWillUpdate', 'componentDidCatch', 'componentDidMount', 'componentDidUpdate', 'componentWillMount', 'componentWillReceiveProps', 'componentWillUnmount', 'componentWillUpdate', 'getDerivedStateFromError', 'getDerivedStateFromProps', 'getSnapshotBeforeUpdate', 'shouldComponentUpdate', ]); const ENVIRONMENT_CALLS = new Set([ 'BroadcastChannel', 'EventSource', 'FileReader', 'IntersectionObserver', 'MutationObserver', 'ResizeObserver', 'SharedWorker', 'WebSocket', 'Worker', 'XMLHttpRequest', 'addEventListener', 'cancelAnimationFrame', 'clearInterval', 'clearTimeout', 'fetch', 'matchMedia', 'queueMicrotask', 'requestAnimationFrame', 'removeEventListener', 'setInterval', 'setTimeout', ]); const ENVIRONMENT_OBJECTS = new Set([ 'document', 'history', 'indexedDB', 'localStorage', 'location', 'navigator', 'sessionStorage', ]); const FORBIDDEN_ENVIRONMENT_IMPORTS = new Set([ 'electron', ...builtinModules, ...builtinModules.map((module) => `node:${module.replace(/^node:/u, '')}`), ]); const SOURCE_EXTENSION = /\.(?:(?:c|m)?(?:js|ts)x?)$/u; const SOURCE_FILE = SOURCE_EXTENSION; const DECLARATION_FILE = /\.d\.(?:(?:c|m)?ts)$/u; const LEGACY_APP_SHELL_FILE = /^(?:app-shell(?:-.+)?|use-app-shell-.+)\.(?:(?:c|m)?(?:js|ts)x?)$/u; const LEGACY_APP_SHELL_MODULE = 'src/renderer/app-shell'; const LEGACY_APP_SHELL_ADAPTER = 'src/renderer/composition/legacy-desktop-region.tsx'; const RENDERER_ENTRY_INDEX = 'src/renderer/index.html'; const RENDERER_ENTRY_SOURCE = 'src/renderer/main.tsx'; const RENDERER_ENTRY_SCRIPT_SOURCE = '/main.tsx'; const RENDERER_LOADER_SOURCE = 'src/main/main-window.ts'; const MAIN_RENDERER_LOADER_SOURCE = 'src/main/main-renderer-loader.ts'; const RENDERER_VITE_CONFIG = 'vite.config.ts'; const RENDERER_BUILD_SCRIPT = 'vite build && node scripts/check-renderer-entry-output.mjs && node ../../scripts/check-third-party-notices.mjs'; const DESKTOP_SELF_PREFIX = '@maka/desktop/'; const CAPABILITY_DEBT_METRICS = [ 'actionFactories', 'bridgePaths', 'dependencyPaths', 'environmentCapabilities', 'hookCalls', 'lifecycleMethods', 'unresolvedDependencies', ]; const ROOT_DEBT_METRICS = [ ...CAPABILITY_DEBT_METRICS, 'importDeclarations', 'importSpecifiers', 'nonTriviaTokens', ]; const DEFAULT_LEGACY_GROWTH_DIRECTORIES = [ 'src/renderer/astryx-theme', 'src/renderer/computer-use-overlay', 'src/renderer/locales', 'src/renderer/settings', ]; const PARSER_PLUGINS = [ 'explicitResourceManagement', 'importAttributes', 'jsx', 'typescript', ]; function normalizePath(value) { return value.split(sep).join('/'); } function sortedObject(value) { return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right))); } function sameJson(left, right) { return JSON.stringify(sortedObject(left)) === JSON.stringify(sortedObject(right)); } function isRecord(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } function isSortedUniqueStrings(value) { return ( Array.isArray(value) && value.every((item) => typeof item === 'string') && JSON.stringify(value) === JSON.stringify([...new Set(value)].sort()) ); } function validateCountMap(value) { return ( isRecord(value) && Object.entries(value).every( ([key, count]) => key.length > 0 && Number.isInteger(count) && count >= 0, ) ); } function validateArchitectureConfig(config, label, violations) { if (!isRecord(config)) { violations.push(`${label} architecture ledger must be an object`); return false; } let valid = true; const reject = (message) => { valid = false; violations.push(`${label} architecture ledger: ${message}`); }; if (config.version !== 1) reject('version must be 1'); if (!isSortedUniqueStrings(config.legacyRendererFiles)) { reject('legacyRendererFiles must be sorted unique strings'); } if (!isSortedUniqueStrings(config.legacyGrowthDirectories)) { reject('legacyGrowthDirectories must be sorted unique strings'); } else if ( config.legacyGrowthDirectories.some( (path) => !path.startsWith('src/renderer/') || path.includes('..') || path.includes('\\'), ) ) { reject('legacyGrowthDirectories must contain normalized renderer directories'); } for (const field of ['legacyFeatureImports', 'legacyPlatformImports']) { if (!isSortedUniqueStrings(config[field])) reject(`${field} must be sorted unique strings`); } if ( !isRecord(config.legacyAppShell) || !isRecord(config.legacyAppShell.files) || !isRecord(config.legacyAppShell.closure) ) { reject('legacyAppShell.files and legacyAppShell.closure must be objects'); } if (!isRecord(config.rootDebt)) reject('rootDebt must be an object'); if (!isRecord(config.rootDebtClosure)) reject('rootDebtClosure must be an object'); if (!Array.isArray(config.ownership)) reject('ownership must be an array'); if (!valid) return false; const debtSections = [ { entries: Object.entries(config.legacyAppShell.files), full: true }, { entries: Object.entries(config.legacyAppShell.closure), full: false }, { entries: Object.entries(config.rootDebt), full: true }, { entries: Object.entries(config.rootDebtClosure), full: false }, ]; for (const section of debtSections) { for (const [path, debt] of section.entries) { const requiredPrefix = section.full ? 'src/renderer/' : 'src/'; if (!path.startsWith(requiredPrefix) || path.includes('..') || path.includes('\\')) { reject(`${path}: debt path must be a normalized ${section.full ? 'renderer' : 'Desktop'} source path`); } if (!isRecord(debt)) { reject(`${path}: debt entry must be an object`); continue; } if (section.full) { for (const metric of ['importDeclarations', 'importSpecifiers', 'nonTriviaTokens']) { if (!Number.isInteger(debt[metric]) || debt[metric] < 0) reject(`${path}: ${metric} must be a non-negative integer`); } } if (!Number.isInteger(debt.unresolvedDependencies) || debt.unresolvedDependencies < 0) { reject(`${path}: unresolvedDependencies must be a non-negative integer`); } for (const metric of ['bridgePaths', 'dependencyPaths', 'environmentCapabilities', 'hookCalls', 'lifecycleMethods']) { if (!validateCountMap(debt[metric])) reject(`${path}: ${metric} must be a non-negative count map`); } if (!isSortedUniqueStrings(debt.actionFactories)) reject(`${path}: actionFactories must be sorted unique strings`); } } const capabilities = new Set(); const targetZone = /^(?:application(?:\/[a-z0-9-]+)*|bootstrap|composition|features\/[a-z0-9-]+|shell|split-by-capability|testing)$/u; for (const owner of config.ownership) { if (!isRecord(owner)) { reject('ownership entries must be objects'); continue; } if (typeof owner.capability !== 'string' || owner.capability.length === 0) reject('ownership capability must be non-empty'); if (capabilities.has(owner.capability)) reject(`duplicate ownership capability ${owner.capability}`); capabilities.add(owner.capability); if (typeof owner.targetZone !== 'string' || !targetZone.test(owner.targetZone)) { reject(`${owner.capability}: unsupported targetZone ${String(owner.targetZone)}`); } if (!isSortedUniqueStrings(owner.legacyPaths)) { reject(`${owner.capability}: legacyPaths must be sorted unique strings`); } } return valid; } function childNodes(node) { const children = []; for (const [key, value] of Object.entries(node)) { if (['comments', 'end', 'errors', 'extra', 'loc', 'start', 'tokens'].includes(key)) continue; if (Array.isArray(value)) { for (const item of value) { if (item && typeof item === 'object' && typeof item.type === 'string') children.push(item); } continue; } if (value && typeof value === 'object' && typeof value.type === 'string') children.push(value); } return children; } function isMemberExpression(node) { return node?.type === 'MemberExpression' || node?.type === 'OptionalMemberExpression'; } function memberName(node) { if (node?.type === 'Identifier') return node.name; if (node?.type === 'StringLiteral') return node.value; return undefined; } function unwrapExpression(node) { let current = node; while ( current && ['ParenthesizedExpression', 'TSAsExpression', 'TSNonNullExpression', 'TSSatisfiesExpression', 'TSTypeAssertion', 'TypeCastExpression'].includes( current.type, ) ) { current = current.expression; } return current; } function memberPropertyName(node) { if (!isMemberExpression(node)) return undefined; const property = unwrapExpression(node.property); if (!node.computed && property?.type === 'Identifier') return property.name; return staticString(property); } function isWindowExpression(node, windowAliases = new Set(['globalThis', 'window'])) { const expression = unwrapExpression(node); if (expression?.type === 'Identifier' && windowAliases.has(expression.name)) return true; return ( isMemberExpression(expression) && unwrapExpression(expression.object)?.type === 'Identifier' && windowAliases.has(unwrapExpression(expression.object).name) && memberPropertyName(expression) === 'window' ); } function isWindowMaka(node, windowAliases) { const expression = unwrapExpression(node); return ( isMemberExpression(expression) && isWindowExpression(expression.object, windowAliases) && memberPropertyName(expression) === 'maka' ); } function hookCallNames( node, hookAliases, hookNamespaces, reactNamespaces, parents, hookBindings, ) { const expression = unwrapExpression(node); if (expression?.type === 'Identifier') { const binding = lexicalBindingIdentifier(expression, expression.name, parents); return binding ? hookBindings.hookAliasBindings.get(binding) : hookAliases.get(expression.name); } if ( isMemberExpression(expression) && unwrapExpression(expression.object)?.type === 'Identifier' ) { const name = memberPropertyName(expression); const namespaceNode = unwrapExpression(expression.object); const namespace = namespaceNode.name; const binding = lexicalBindingIdentifier(namespaceNode, namespace, parents); const reactNamespace = binding ? hookBindings.reactNamespaceBindings.has(binding) : reactNamespaces.has(namespace); const hookNamespace = binding ? hookBindings.hookNamespaceBindings.has(binding) : hookNamespaces.has(namespace); if (reactNamespace && name && STATEFUL_HOOKS.has(name)) return new Set([name]); if (hookNamespace && name && /^use[A-Z0-9]/u.test(name)) return new Set([name]); } return undefined; } function addHookNames(aliases, key, names) { if (!names) return false; let known = aliases.get(key); if (!known) { known = new Set(); aliases.set(key, known); } let changed = false; for (const name of names) { if (known.has(name)) continue; known.add(name); changed = true; } return changed; } function staticString(node) { if (node?.type === 'StringLiteral') return node.value; if (node?.type === 'BinaryExpression' && node.operator === '+') { const left = staticString(node.left); const right = staticString(node.right); return left !== undefined && right !== undefined ? `${left}${right}` : undefined; } if (node?.type === 'TemplateLiteral') { let value = ''; for (let index = 0; index < node.quasis.length; index += 1) { value += node.quasis[index]?.value.cooked ?? node.quasis[index]?.value.raw ?? ''; if (index < node.expressions.length) { const expression = staticString(node.expressions[index]); if (expression === undefined) return undefined; value += expression; } } return value; } return undefined; } function environmentPath( node, windowAliases, environmentAliases = new Map(), parents = new WeakMap(), ) { const expression = unwrapExpression(node); if (expression?.type === 'Identifier') { if (environmentAliases.has(expression.name)) return environmentAliases.get(expression.name); if (windowAliases.has(expression.name)) return 'window'; if (ENVIRONMENT_OBJECTS.has(expression.name) && !environmentIdentifierIsShadowed(expression, expression.name, parents)) { return expression.name; } return undefined; } if (!isMemberExpression(expression)) return undefined; const base = environmentPath( expression.object, windowAliases, environmentAliases, parents, ); const property = memberPropertyName(expression); if (!base || !property) return undefined; return `${base}.${property}`; } function bridgePath(node, windowAliases, bridgeAliases = new Map()) { const expression = unwrapExpression(node); if (expression?.type === 'Identifier') return bridgeAliases.get(expression.name); if (isWindowMaka(expression, windowAliases)) return 'window.maka'; if (!isMemberExpression(expression)) return undefined; const base = bridgePath(expression.object, windowAliases, bridgeAliases); const property = memberPropertyName(expression); if (!base || !property) return undefined; return `${base}.${property}`; } function sourceDependency(node) { if ( (node.type === 'ImportDeclaration' || node.type === 'ExportAllDeclaration' || node.type === 'ExportNamedDeclaration') && staticString(node.source) !== undefined ) { return staticString(node.source); } if (node.type === 'ImportExpression' && staticString(node.source) !== undefined) { return staticString(node.source); } if ( node.type === 'TSImportEqualsDeclaration' && node.moduleReference?.type === 'TSExternalModuleReference' && staticString(node.moduleReference.expression) !== undefined ) { return staticString(node.moduleReference.expression); } if (node.type === 'TSImportType' && staticString(node.argument) !== undefined) { return staticString(node.argument); } if ( (node.type === 'CallExpression' || node.type === 'OptionalCallExpression') && staticString(node.arguments?.[0]) !== undefined && (node.callee?.type === 'Import' || (node.callee?.type === 'Identifier' && node.callee.name === 'require')) ) { return staticString(node.arguments[0]); } return undefined; } function bindingNames(pattern) { if (!pattern) return []; if (pattern.type === 'Identifier') return [pattern.name]; if (pattern.type === 'AssignmentPattern') return bindingNames(pattern.left); if (pattern.type === 'RestElement') return bindingNames(pattern.argument); if (pattern.type === 'TSParameterProperty') return bindingNames(pattern.parameter); if (pattern.type === 'ArrayPattern') return pattern.elements.flatMap((element) => bindingNames(element)); if (pattern.type === 'ObjectPattern') { return pattern.properties.flatMap((property) => property.type === 'RestElement' ? bindingNames(property.argument) : bindingNames(property.value), ); } return []; } function buildParentMap(root) { const parents = new WeakMap(); function visit(node) { for (const child of childNodes(node)) { parents.set(child, node); visit(child); } } visit(root); return parents; } function statementBindings(statement) { const declaration = statement?.type === 'ExportNamedDeclaration' || statement?.type === 'ExportDefaultDeclaration' ? statement.declaration : statement; if (!declaration) return []; if (declaration.type === 'ImportDeclaration') return declaration.specifiers.map((specifier) => specifier.local.name); if (declaration.type === 'VariableDeclaration') { return declaration.declarations.flatMap((item) => bindingNames(item.id)); } if (declaration.type === 'FunctionDeclaration' || declaration.type === 'ClassDeclaration') { return bindingNames(declaration.id); } return []; } function bindingIdentifier(pattern, name) { if (!pattern) return undefined; if (pattern.type === 'Identifier') return pattern.name === name ? pattern : undefined; if (pattern.type === 'AssignmentPattern') return bindingIdentifier(pattern.left, name); if (pattern.type === 'RestElement') return bindingIdentifier(pattern.argument, name); if (pattern.type === 'TSParameterProperty') return bindingIdentifier(pattern.parameter, name); if (pattern.type === 'ArrayPattern') { return pattern.elements .map((element) => bindingIdentifier(element, name)) .find(Boolean); } if (pattern.type === 'ObjectPattern') { return pattern.properties .map((property) => property.type === 'RestElement' ? bindingIdentifier(property.argument, name) : bindingIdentifier(property.value, name), ) .find(Boolean); } return undefined; } function statementBindingIdentifier(statement, name, includeVar = true) { const declaration = statement?.type === 'ExportNamedDeclaration' || statement?.type === 'ExportDefaultDeclaration' ? statement.declaration : statement; if (!declaration) return undefined; if (declaration.type === 'ImportDeclaration') { return declaration.specifiers .map((specifier) => specifier.local) .find((local) => local.name === name); } if (declaration.type === 'VariableDeclaration') { if (!includeVar && declaration.kind === 'var') return undefined; return declaration.declarations .map((item) => bindingIdentifier(item.id, name)) .find(Boolean); } if (declaration.type === 'FunctionDeclaration' || declaration.type === 'ClassDeclaration') { return bindingIdentifier(declaration.id, name); } return undefined; } function hoistedVarBindingIdentifier(root, name) { let found; function visit(node, isRoot = false) { if (!node || found) return; if ( !isRoot && [ 'ArrowFunctionExpression', 'ClassDeclaration', 'ClassExpression', 'ClassMethod', 'ClassPrivateMethod', 'FunctionDeclaration', 'FunctionExpression', 'ObjectMethod', 'StaticBlock', ].includes(node.type) ) { return; } if (node.type === 'VariableDeclaration' && node.kind === 'var') { found = node.declarations .map((item) => bindingIdentifier(item.id, name)) .find(Boolean); if (found) return; } for (const child of childNodes(node)) visit(child); } visit(root, true); return found; } function lexicalBindingIdentifier(node, name, parents) { let current = parents.get(node); while (current) { if ( current.type === 'FunctionDeclaration' || current.type === 'FunctionExpression' || current.type === 'ArrowFunctionExpression' || current.type === 'ObjectMethod' || current.type === 'ClassMethod' || current.type === 'ClassPrivateMethod' ) { const parameter = current.params .map((item) => bindingIdentifier(item, name)) .find(Boolean); if (parameter) return parameter; if (current.type !== 'ArrowFunctionExpression' && !current.type.endsWith('Method')) { const functionName = bindingIdentifier(current.id, name); if (functionName) return functionName; } const hoisted = hoistedVarBindingIdentifier(current.body, name); if (hoisted) return hoisted; } if (current.type === 'CatchClause') { const caught = bindingIdentifier(current.param, name); if (caught) return caught; } if (current.type === 'Program' || current.type === 'BlockStatement') { const declared = current.body .map((statement) => statementBindingIdentifier(statement, name, false)) .find(Boolean); if (declared) return declared; if (current.type === 'Program') { const hoisted = hoistedVarBindingIdentifier(current, name); if (hoisted) return hoisted; } } if ( current.type === 'ForStatement' && current.init?.type === 'VariableDeclaration' && current.init.kind !== 'var' ) { const declared = statementBindingIdentifier(current.init, name, false); if (declared) return declared; } if ( (current.type === 'ForInStatement' || current.type === 'ForOfStatement') && current.left?.type === 'VariableDeclaration' && current.left.kind !== 'var' ) { const declared = statementBindingIdentifier(current.left, name, false); if (declared) return declared; } current = parents.get(current); } return undefined; } function environmentIdentifierIsShadowed(node, name, parents) { let current = parents.get(node); while (current) { if ( current.type === 'FunctionDeclaration' || current.type === 'FunctionExpression' || current.type === 'ArrowFunctionExpression' || current.type === 'ObjectMethod' || current.type === 'ClassMethod' || current.type === 'ClassPrivateMethod' ) { if ( current.type !== 'ArrowFunctionExpression' && !current.type.endsWith('Method') && bindingNames(current.id).includes(name) ) { return true; } if (current.params.some((parameter) => bindingNames(parameter).includes(name))) return true; } if (current.type === 'CatchClause' && bindingNames(current.param).includes(name)) return true; if (current.type === 'Program' || current.type === 'BlockStatement') { if (current.body.some((statement) => statementBindings(statement).includes(name))) return true; } if ( current.type === 'ForStatement' && current.init?.type === 'VariableDeclaration' && statementBindings(current.init).includes(name) ) { return true; } if ( (current.type === 'ForInStatement' || current.type === 'ForOfStatement') && current.left?.type === 'VariableDeclaration' && statementBindings(current.left).includes(name) ) { return true; } current = parents.get(current); } return false; } function collectAliases(program, parents) { const hookAliases = new Map(); const hookAliasBindings = new Map(); const hookNamespaces = new Set(); const hookNamespaceBindings = new WeakSet(); const reactComponentBases = new Set(); const reactNamespaces = new Set(); const reactNamespaceBindings = new WeakSet(); const hookBindings = { hookAliasBindings, hookNamespaceBindings, reactNamespaceBindings, }; for (const statement of program.body) { if (statement.type !== 'ImportDeclaration') continue; const isReact = statement.source.value === 'react'; for (const specifier of statement.specifiers) { if (isReact && (specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportNamespaceSpecifier')) { reactNamespaces.add(specifier.local.name); reactNamespaceBindings.add(specifier.local); } else if (isReact && specifier.type === 'ImportSpecifier') { const imported = memberName(specifier.imported); if (imported && STATEFUL_HOOKS.has(imported)) { addHookNames(hookAliases, specifier.local.name, [imported]); addHookNames(hookAliasBindings, specifier.local, [imported]); } if (imported === 'Component' || imported === 'PureComponent') reactComponentBases.add(specifier.local.name); } else if (!isReact) { const imported = specifier.type === 'ImportSpecifier' ? memberName(specifier.imported) : specifier.local.name; if (imported && /^use[A-Z0-9]/u.test(imported)) { addHookNames(hookAliases, specifier.local.name, [imported]); addHookNames(hookAliasBindings, specifier.local, [imported]); } if (specifier.type === 'ImportNamespaceSpecifier') { hookNamespaces.add(specifier.local.name); hookNamespaceBindings.add(specifier.local); } } } } const windowAliases = new Set(['globalThis', 'self', 'window']); const bridgeAliases = new Map(); const environmentAliases = new Map(); let changed = true; while (changed) { changed = false; const addAlias = (aliases, name, path) => { if (!path || aliases.has(name)) return; aliases.set(name, path); changed = true; }; function visit(node) { if ( node.type === 'ClassDeclaration' && node.id?.type === 'Identifier' && isReactComponentClass(node, reactComponentBases, reactNamespaces) && !reactComponentBases.has(node.id.name) ) { reactComponentBases.add(node.id.name); changed = true; } if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') { const declaredBinding = lexicalBindingIdentifier(node.id, node.id.name, parents) ?? node.id; const init = unwrapExpression(node.init); const initBinding = init?.type === 'Identifier' ? lexicalBindingIdentifier(init, init.name, parents) : undefined; const initIsReactNamespace = init?.type === 'Identifier' && (initBinding ? reactNamespaceBindings.has(initBinding) : reactNamespaces.has(init.name)); const initIsHookNamespace = init?.type === 'Identifier' && (initBinding ? hookNamespaceBindings.has(initBinding) : hookNamespaces.has(init.name)); if (initIsReactNamespace && !reactNamespaceBindings.has(declaredBinding)) { reactNamespaces.add(node.id.name); reactNamespaceBindings.add(declaredBinding); changed = true; } if (initIsHookNamespace && !hookNamespaceBindings.has(declaredBinding)) { hookNamespaces.add(node.id.name); hookNamespaceBindings.add(declaredBinding); changed = true; } if (isWindowExpression(node.init, windowAliases) && !windowAliases.has(node.id.name)) { windowAliases.add(node.id.name); changed = true; } addAlias(bridgeAliases, node.id.name, bridgePath(node.init, windowAliases, bridgeAliases)); const environment = environmentPath( node.init, windowAliases, environmentAliases, parents, ); if (environment && !environment.startsWith('window.maka')) { addAlias(environmentAliases, node.id.name, environment); } if ( node.init?.type === 'Identifier' && ENVIRONMENT_CALLS.has(node.init.name) && !environmentIdentifierIsShadowed(node.init, node.init.name, parents) ) { addAlias(environmentAliases, node.id.name, node.init.name); } const hooks = hookCallNames( node.init, hookAliases, hookNamespaces, reactNamespaces, parents, hookBindings, ); const nameChanged = addHookNames(hookAliases, node.id.name, hooks); const bindingChanged = addHookNames( hookAliasBindings, declaredBinding, hooks, ); if (nameChanged || bindingChanged) { changed = true; } } if (node.type === 'VariableDeclarator' && node.id?.type === 'ObjectPattern') { const bridgeBase = bridgePath(node.init, windowAliases, bridgeAliases); const environmentBase = environmentPath( node.init, windowAliases, environmentAliases, parents, ); for (const property of node.id.properties) { if (property.type !== 'ObjectProperty' || property.computed || property.value?.type !== 'Identifier') continue; const propertyName = memberName(property.key); if (!propertyName) continue; if (bridgeBase) addAlias(bridgeAliases, property.value.name, `${bridgeBase}.${propertyName}`); if (isWindowExpression(node.init, windowAliases) && propertyName === 'maka') { addAlias(bridgeAliases, property.value.name, 'window.maka'); } if (environmentBase && !environmentBase.startsWith('window.maka')) { addAlias(environmentAliases, property.value.name, `${environmentBase}.${propertyName}`); } } } if (node.type === 'AssignmentExpression' && node.left?.type === 'Identifier') { const right = unwrapExpression(node.right); const rightBinding = right?.type === 'Identifier' ? lexicalBindingIdentifier(right, right.name, parents) : undefined; const leftBinding = lexicalBindingIdentifier(node.left, node.left.name, parents); const rightIsReactNamespace = right?.type === 'Identifier' && (rightBinding ? reactNamespaceBindings.has(rightBinding) : reactNamespaces.has(right.name)); const rightIsHookNamespace = right?.type === 'Identifier' && (rightBinding ? hookNamespaceBindings.has(rightBinding) : hookNamespaces.has(right.name)); if ( rightIsReactNamespace && (leftBinding ? !reactNamespaceBindings.has(leftBinding) : !reactNamespaces.has(node.left.name)) ) { reactNamespaces.add(node.left.name); if (leftBinding) reactNamespaceBindings.add(leftBinding); changed = true; } if ( rightIsHookNamespace && (leftBinding ? !hookNamespaceBindings.has(leftBinding) : !hookNamespaces.has(node.left.name)) ) { hookNamespaces.add(node.left.name); if (leftBinding) hookNamespaceBindings.add(leftBinding); changed = true; } addAlias(bridgeAliases, node.left.name, bridgePath(node.right, windowAliases, bridgeAliases)); const environment = environmentPath( node.right, windowAliases, environmentAliases, parents, ); if (environment && !environment.startsWith('window.maka')) { addAlias(environmentAliases, node.left.name, environment); } if ( node.right?.type === 'Identifier' && ENVIRONMENT_CALLS.has(node.right.name) && !environmentIdentifierIsShadowed(node.right, node.right.name, parents) ) { addAlias(environmentAliases, node.left.name, node.right.name); } const hooks = hookCallNames( node.right, hookAliases, hookNamespaces, reactNamespaces, parents, hookBindings, ); const nameChanged = addHookNames(hookAliases, node.left.name, hooks); const bindingChanged = leftBinding ? addHookNames(hookAliasBindings, leftBinding, hooks) : false; if (nameChanged || bindingChanged) { changed = true; } } if ( node.type === 'VariableDeclarator' && node.id?.type === 'ObjectPattern' && unwrapExpression(node.init)?.type === 'Identifier' ) { const namespaceNode = unwrapExpression(node.init); const namespace = namespaceNode.name; const namespaceBinding = lexicalBindingIdentifier( namespaceNode, namespace, parents, ); const reactNamespace = namespaceBinding ? reactNamespaceBindings.has(namespaceBinding) : reactNamespaces.has(namespace); const hookNamespace = namespaceBinding ? hookNamespaceBindings.has(namespaceBinding) : hookNamespaces.has(namespace); if (reactNamespace || hookNamespace) { for (const property of node.id.properties) { if (property.type !== 'ObjectProperty' || property.computed) continue; const imported = memberName(property.key); const recognized = reactNamespace ? imported && STATEFUL_HOOKS.has(imported) : imported && /^use[A-Z0-9]/u.test(imported); const local = property.value?.type === 'Identifier' ? property.value : property.value?.type === 'AssignmentPattern' && property.value.left?.type === 'Identifier' ? property.value.left : undefined; if (recognized && local) { const localBinding = lexicalBindingIdentifier(local, local.name, parents) ?? local; const nameChanged = addHookNames(hookAliases, local.name, [imported]); const bindingChanged = addHookNames( hookAliasBindings, localBinding, [imported], ); if (nameChanged || bindingChanged) { changed = true; } } } } } for (const child of childNodes(node)) visit(child); } visit(program); } return { bridgeAliases, environmentAliases, hookAliases, hookBindings, hookNamespaces, reactComponentBases, reactNamespaces, windowAliases, }; } function isReactComponentClass(node, reactComponentBases, reactNamespaces) { if (node?.type !== 'ClassDeclaration' && node?.type !== 'ClassExpression') return false; const superClass = unwrapExpression(node.superClass); if (superClass?.type === 'Identifier') return reactComponentBases.has(superClass.name); return ( isMemberExpression(superClass) && unwrapExpression(superClass.object)?.type === 'Identifier' && reactNamespaces.has(unwrapExpression(superClass.object).name) && ['Component', 'PureComponent'].includes(memberPropertyName(superClass)) ); } function enclosingClass(node, parents) { let current = parents.get(node); while (current) { if (current.type === 'ClassDeclaration' || current.type === 'ClassExpression') return current; current = parents.get(current); } return undefined; } export function analyzeRendererSource(source, file = 'fixture.ts') { const typedSource = /\.(?:(?:c|m)?ts|tsx)$/u.test(file); const jsxSource = /\.(?:jsx|tsx)$/u.test(file); const ast = parse(source, { createImportExpressions: true, errorRecovery: false, plugins: PARSER_PLUGINS.filter((plugin) => (plugin === 'typescript' ? typedSource : plugin === 'jsx' ? jsxSource : true)), sourceFilename: file, sourceType: 'module', tokens: true, }); const parents = buildParentMap(ast.program); const { bridgeAliases, environmentAliases, hookAliases, hookBindings, hookNamespaces, reactComponentBases, reactNamespaces, windowAliases, } = collectAliases(ast.program, parents); const bridgePaths = {}; const environmentCapabilities = {}; const hookCalls = {}; const lifecycleMethods = {}; const actionFactories = []; const dependencies = []; const dependencyPaths = {}; let importDeclarations = 0; let importSpecifiers = 0; let unresolvedDependencies = 0; function recordBridgePath(path) { bridgePaths[path] = (bridgePaths[path] ?? 0) + 1; } function recordEnvironmentCapability(path) { environmentCapabilities[path] = (environmentCapabilities[path] ?? 0) + 1; } function visit(node, parent) { if (node.type === 'ImportDeclaration') { importDeclarations += 1; importSpecifiers += node.specifiers.length; } if (node.type === 'TSImportEqualsDeclaration') { importDeclarations += 1; importSpecifiers += 1; } const dependency = sourceDependency(node); if (dependency !== undefined) { dependencies.push(dependency); dependencyPaths[dependency] = (dependencyPaths[dependency] ?? 0) + 1; } if ( (node.type === 'ImportExpression' && staticString(node.source) === undefined) || ((node.type === 'CallExpression' || node.type === 'OptionalCallExpression') && (node.callee?.type === 'Import' || (node.callee?.type === 'Identifier' && node.callee.name === 'require')) && staticString(node.arguments?.[0]) === undefined) || ((node.type === 'CallExpression' || node.type === 'OptionalCallExpression') && isMemberExpression(node.callee) && unwrapExpression(node.callee.object)?.type === 'MetaProperty' && unwrapExpression(node.callee.object).meta?.name === 'import' && unwrapExpression(node.callee.object).property?.name === 'meta' && ['glob', 'globEager'].includes(memberPropertyName(node.callee))) ) { unresolvedDependencies += 1; } const directEnvironmentPath = environmentPath( node, windowAliases, environmentAliases, parents, ); if ( directEnvironmentPath && !directEnvironmentPath.startsWith('window.maka') && !(isMemberExpression(parent) && unwrapExpression(parent.object) === node) ) { recordEnvironmentCapability(directEnvironmentPath); } if (isMemberExpression(node) && memberPropertyName(node) === undefined) { const dynamicBridgeBase = bridgePath(node.object, windowAliases, bridgeAliases); if (dynamicBridgeBase) recordBridgePath(`${dynamicBridgeBase}.*`); const dynamicEnvironmentBase = environmentPath( node.object, windowAliases, environmentAliases, parents, ); if (dynamicEnvironmentBase && !dynamicEnvironmentBase.startsWith('window.maka')) { recordEnvironmentCapability(`${dynamicEnvironmentBase}.*`); } } const directBridgePath = bridgePath(node, windowAliases, bridgeAliases); if ( directBridgePath && !(isMemberExpression(parent) && unwrapExpression(parent.object) === node) && !(node.type === 'Identifier' && parent?.type === 'VariableDeclarator' && parent.id === node) ) { recordBridgePath(directBridgePath); } if ( node.type === 'VariableDeclarator' && node.id?.type === 'ObjectPattern' && isWindowExpression(node.init, windowAliases) ) { for (const property of node.id.properties) { if (property.type === 'ObjectProperty' && !property.computed && memberName(property.key) === 'maka') { recordBridgePath('window.maka'); } } } if (node.type === 'CallExpression' || node.type === 'OptionalCallExpression') { const names = hookCallNames( node.callee, hookAliases, hookNamespaces, reactNamespaces, parents, hookBindings, ); for (const name of names ?? []) { hookCalls[name] = (hookCalls[name] ?? 0) + 1; } const callee = unwrapExpression(node.callee); if ( callee?.type === 'Identifier' && ENVIRONMENT_CALLS.has(callee.name) && !environmentIdentifierIsShadowed(callee, callee.name, parents) ) { recordEnvironmentCapability(callee.name); } } if (node.type === 'NewExpression') { const callee = unwrapExpression(node.callee); if ( callee?.type === 'Identifier' && ENVIRONMENT_CALLS.has(callee.name) && !environmentIdentifierIsShadowed(callee, callee.name, parents) ) { recordEnvironmentCapability(callee.name); } } const ownerClass = enclosingClass(node, parents); const reactComponentOwner = isReactComponentClass(ownerClass, reactComponentBases, reactNamespaces); if (reactComponentOwner && (node.type === 'ClassMethod' || node.type === 'ClassPrivateMethod')) { const name = memberName(node.key); if (name && REACT_LIFECYCLE_METHODS.has(name)) { lifecycleMethods[name] = (lifecycleMethods[name] ?? 0) + 1; } } if (reactComponentOwner && ['ClassProperty', 'ClassPrivateProperty', 'PropertyDefinition'].includes(node.type)) { const name = memberName(node.key); if (name === 'state' || (name && REACT_LIFECYCLE_METHODS.has(name))) { lifecycleMethods[name] = (lifecycleMethods[name] ?? 0) + 1; } } if ( reactComponentOwner && isMemberExpression(node) && unwrapExpression(node.object)?.type === 'ThisExpression' && ['setState', 'state'].includes(memberPropertyName(node)) ) { const name = memberPropertyName(node); lifecycleMethods[name] = (lifecycleMethods[name] ?? 0) + 1; } if ( (node.type === 'FunctionDeclaration' || node.type === 'VariableDeclarator') && node.id?.type === 'Identifier' && node.id.name.startsWith('createAppShell') ) { actionFactories.push(node.id.name); } for (const child of childNodes(node)) { parents.set(child, node); visit(child, node); } } visit(ast.program, undefined); return { actionFactories: actionFactories.sort(), bridgePaths: sortedObject(bridgePaths), dependencies, dependencyPaths: sortedObject(dependencyPaths), environmentCapabilities: sortedObject(environmentCapabilities), hookCalls: sortedObject(hookCalls), importDeclarations, importSpecifiers, lifecycleMethods: sortedObject(lifecycleMethods), nonTriviaTokens: ast.tokens.length, unresolvedDependencies, }; } function sourceFiles(root, { includeDeclarations = false } = {}) { if (!existsSync(root)) return []; return readdirSync(root, { withFileTypes: true }).flatMap((entry) => { const path = join(root, entry.name); if (entry.isDirectory()) return sourceFiles(path, { includeDeclarations }); return SOURCE_FILE.test(entry.name) && (includeDeclarations || !DECLARATION_FILE.test(entry.name)) ? [path] : []; }); } function zoneFor(path) { const normalized = normalizePath(path).replace(SOURCE_EXTENSION, ''); if (normalized.startsWith('src/main/')) return { kind: 'main' }; if (normalized.startsWith('src/preload/')) return { kind: 'preload' }; if (!normalized.startsWith('src/renderer/')) return { kind: 'external' }; const feature = normalized.match(/^src\/renderer\/features\/([^/]+)(?:\/(.*))?$/u); if (feature) return { kind: 'feature', name: feature[1], subpath: feature[2] ?? '' }; for (const kind of ['application', 'bootstrap', 'composition', 'shell']) { const prefix = `src/renderer/${kind}/`; if (normalized.startsWith(prefix)) return { kind, subpath: normalized.slice(prefix.length) }; } if (normalized.startsWith('src/renderer/platform/desktop/')) return { kind: 'platform' }; return { kind: 'legacy' }; } function isRootClosureDebtSource(path) { return ['external', 'legacy', 'main', 'preload'].includes(zoneFor(path).kind); } function resolveDependency(desktopRoot, importer, dependency) { let target; if (dependency.startsWith('.')) target = resolve(dirname(importer), dependency); else if (dependency.startsWith(DESKTOP_SELF_PREFIX)) target = resolve(desktopRoot, dependency.slice(DESKTOP_SELF_PREFIX.length)); else return undefined; const normalized = normalizePath(target); return normalized.replace(/\.(?:c|m)?(?:js|ts)x?$/u, ''); } function sourceFileIndex(desktopRoot) { const index = new Map(); const paths = sourceFiles(resolve(desktopRoot, 'src'), { includeDeclarations: true }).sort(); for (const path of paths.filter((path) => !DECLARATION_FILE.test(path))) { const key = normalizePath(path).replace(SOURCE_EXTENSION, ''); if (!index.has(key)) index.set(key, path); } for (const path of paths.filter((path) => DECLARATION_FILE.test(path))) { const key = normalizePath(path).replace(SOURCE_EXTENSION, ''); index.set(key, path); const runtimeAlias = key.replace(/\.d$/u, ''); if (!index.has(runtimeAlias)) index.set(runtimeAlias, path); } return index; } function resolveSourceFile(desktopRoot, importer, dependency, index) { const target = resolveDependency(desktopRoot, importer, dependency); if (!target) return undefined; return index.get(target) ?? index.get(`${target}/index`); } function isSourceLikeDependency(dependency) { const withoutQuery = dependency.split(/[?#]/u, 1)[0]; const lastSegment = withoutQuery.split('/').at(-1) ?? ''; return !lastSegment.includes('.') || SOURCE_EXTENSION.test(lastSegment); } function isAllowedLegacyGrowthPath(config, path) { return config.legacyGrowthDirectories.some((directory) => path.startsWith(`${directory}/`)); } function capabilityDebtMetrics(analysis) { return { bridgePaths: analysis.bridgePaths, environmentCapabilities: analysis.environmentCapabilities, hookCalls: analysis.hookCalls, lifecycleMethods: analysis.lifecycleMethods, unresolvedDependencies: analysis.unresolvedDependencies, actionFactories: analysis.actionFactories, dependencyPaths: analysis.dependencyPaths, }; } function debtMetrics(analysis) { return { importDeclarations: analysis.importDeclarations, ...capabilityDebtMetrics(analysis), importSpecifiers: analysis.importSpecifiers, nonTriviaTokens: analysis.nonTriviaTokens, }; } function collectRootDependencyClosure(desktopRoot, rootPaths, violations = [], label = 'renderer root') { const index = sourceFileIndex(desktopRoot); const sourceRoot = normalizePath(resolve(desktopRoot, 'src')); const roots = new Set(rootPaths); const queue = rootPaths.map((path) => resolve(desktopRoot, path)); const seen = new Set(); const closure = new Set(); while (queue.length > 0) { const file = queue.shift(); const fileKey = normalizePath(file).replace(SOURCE_EXTENSION, ''); if (seen.has(fileKey) || !existsSync(file)) continue; seen.add(fileKey); const fileRelative = normalizePath(relative(desktopRoot, file)); let analysis; try { analysis = analyzeRendererSource(readFileSync(file, 'utf8'), fileRelative); } catch (error) { violations.push(`${fileRelative}: could not parse ${label} dependency closure source: ${error instanceof Error ? error.message : String(error)}`); continue; } if (analysis.unresolvedDependencies > 0) { violations.push(`${fileRelative}: ${label} closure contains a non-static import, require, or import.meta glob`); } for (const dependency of analysis.dependencies) { const unresolvedTarget = resolveDependency(desktopRoot, file, dependency); if (!unresolvedTarget) continue; const normalizedTarget = normalizePath(unresolvedTarget); if (normalizedTarget !== sourceRoot && !normalizedTarget.startsWith(`${sourceRoot}/`)) continue; const target = resolveSourceFile(desktopRoot, file, dependency, index); if (!target) { if (isSourceLikeDependency(dependency)) { violations.push(`${fileRelative}: ${label} closure dependency does not resolve to Desktop source: ${dependency}`); } continue; } const targetRelative = normalizePath(relative(desktopRoot, target)); if (!seen.has(normalizePath(target).replace(SOURCE_EXTENSION, ''))) queue.push(target); if (isRootClosureDebtSource(targetRelative) && !DECLARATION_FILE.test(target) && !roots.has(targetRelative)) { closure.add(targetRelative); } } } return [...closure].sort(); } function featureForAbsolutePath(desktopRoot, absolutePath) { const rendererFeatures = normalizePath(resolve(desktopRoot, 'src/renderer/features')); if (absolutePath !== rendererFeatures && !absolutePath.startsWith(`${rendererFeatures}/`)) return undefined; const rest = absolutePath.slice(rendererFeatures.length).replace(/^\//u, ''); const [name, ...segments] = rest.split('/'); return { name, subpath: segments.join('/') }; } function isTestConsumer(path) { const normalized = normalizePath(path); return ( normalized.includes('/__tests__/') || normalized.includes('/e2e/') || normalized.includes('/stories/') || /\.(?:spec|stories|test)\.[^/]+$/u.test(normalized) ); } function isPublicFeaturePath(subpath) { return subpath === '' || subpath === 'index'; } function isFeatureTestSupportEntry(subpath) { return subpath === 'stories' || subpath === 'testing'; } function isApplicationContractPath(path) { return path === 'src/renderer/application/contracts' || path.startsWith('src/renderer/application/contracts/'); } function isPublicApplicationPath(path) { if (isApplicationContractPath(path)) return true; return /^src\/renderer\/application\/[^/]+(?:\/index)?$/u.test(path); } function isForbiddenEnvironmentDependency(dependency) { return FORBIDDEN_ENVIRONMENT_IMPORTS.has(dependency) || dependency.startsWith('electron/'); } function validateDependencies({ allowedLegacyFeatureImports, allowedLegacyPlatformImports, analysis, desktopRoot, file, fileRelative, observedLegacyFeatureImports, observedLegacyPlatformImports, violations, }) { const sourceZone = zoneFor(fileRelative); for (const dependency of analysis.dependencies) { if ( ['application', 'bootstrap', 'composition', 'feature', 'platform', 'shell'].includes(sourceZone.kind) && !isTestConsumer(file) && isForbiddenEnvironmentDependency(dependency) ) { violations.push(`${fileRelative}: ${sourceZone.kind} code imports forbidden environment module: ${dependency}`); continue; } const target = resolveDependency(desktopRoot, file, dependency); if (!target) continue; const targetRelative = normalizePath(relative(desktopRoot, target)); const targetZone = zoneFor(targetRelative); const targetFeature = featureForAbsolutePath(desktopRoot, target); if (targetFeature) { if (sourceZone.kind === 'feature' && sourceZone.name !== targetFeature.name) { violations.push(`${fileRelative}: feature ${sourceZone.name} imports feature ${targetFeature.name}: ${dependency}`); continue; } if ( sourceZone.kind === 'feature' && sourceZone.name === targetFeature.name && isFeatureTestSupportEntry(targetFeature.subpath) && !isFeatureTestSupportEntry(sourceZone.subpath) && !isTestConsumer(file) ) { violations.push( `${fileRelative}: feature production code cannot import its ${targetFeature.subpath} entry: ${dependency}`, ); } if (sourceZone.kind !== 'feature') { const supportEntry = isFeatureTestSupportEntry(targetFeature.subpath); if (!isPublicFeaturePath(targetFeature.subpath) && !(supportEntry && isTestConsumer(file))) { violations.push( `${fileRelative}: feature imports must use index${isTestConsumer(file) ? ', testing, or stories' : ''}: ${dependency}`, ); } if (supportEntry && !isTestConsumer(file)) { violations.push( `${fileRelative}: production code imports a feature ${targetFeature.subpath} entry: ${dependency}`, ); } } } if (sourceZone.kind === 'feature') { if (targetZone.kind === 'legacy') { const edge = `${fileRelative} -> ${targetRelative}`; observedLegacyFeatureImports.add(edge); if (!allowedLegacyFeatureImports.has(edge)) { violations.push(`${fileRelative}: feature imports unbudgeted renderer legacy code: ${dependency}`); } } if (targetZone.kind === 'application' && !isApplicationContractPath(targetRelative)) { violations.push(`${fileRelative}: feature imports application implementation instead of a contract: ${dependency}`); } if (['bootstrap', 'composition', 'main', 'platform', 'preload', 'shell'].includes(targetZone.kind)) { violations.push(`${fileRelative}: feature imports a forbidden Desktop/shell module: ${dependency}`); } } if (sourceZone.kind === 'shell') { const allowedApplicationContract = isApplicationContractPath(targetRelative); if ( targetZone.kind === 'feature' || targetZone.kind === 'platform' || targetZone.kind === 'legacy' || targetZone.kind === 'bootstrap' || targetZone.kind === 'composition' || (targetZone.kind === 'application' && !allowedApplicationContract) || targetZone.kind === 'main' || targetZone.kind === 'preload' ) { violations.push(`${fileRelative}: shell imports forbidden implementation module: ${dependency}`); } } if (sourceZone.kind === 'application') { if ( sourceZone.subpath === 'contracts' || sourceZone.subpath.startsWith('contracts/') ) { if (targetZone.kind === 'application' && !isApplicationContractPath(targetRelative)) { violations.push(`${fileRelative}: application contracts import application implementation: ${dependency}`); } } if ( targetZone.kind === 'feature' || targetZone.kind === 'platform' || targetZone.kind === 'shell' || targetZone.kind === 'bootstrap' || targetZone.kind === 'composition' || targetZone.kind === 'legacy' || targetZone.kind === 'main' || targetZone.kind === 'preload' ) { violations.push(`${fileRelative}: application authority imports an outer implementation: ${dependency}`); } } if (sourceZone.kind === 'composition') { if (targetZone.kind === 'application' && !isPublicApplicationPath(targetRelative)) { violations.push(`${fileRelative}: composition imports application implementation instead of a public entry: ${dependency}`); } const allowedLegacyAdapter = fileRelative === LEGACY_APP_SHELL_ADAPTER && targetRelative === LEGACY_APP_SHELL_MODULE; if ( targetZone.kind === 'bootstrap' || targetZone.kind === 'main' || targetZone.kind === 'preload' || (targetZone.kind === 'legacy' && !allowedLegacyAdapter) ) { violations.push(`${fileRelative}: composition imports a forbidden implementation module: ${dependency}`); } } if (sourceZone.kind === 'bootstrap') { if (!['bootstrap', 'composition', 'external', 'platform'].includes(targetZone.kind)) { violations.push(`${fileRelative}: bootstrap imports outside composition/platform: ${dependency}`); } } if (sourceZone.kind === 'platform') { if (targetZone.kind === 'application' && !isPublicApplicationPath(targetRelative)) { violations.push(`${fileRelative}: Desktop adapter imports application implementation instead of a public entry: ${dependency}`); } if (targetZone.kind === 'legacy') { const edge = `${fileRelative} -> ${targetRelative}`; observedLegacyPlatformImports.add(edge); if (!allowedLegacyPlatformImports.has(edge)) { violations.push(`${fileRelative}: Desktop adapter imports unbudgeted renderer legacy code: ${dependency}`); } } if (['bootstrap', 'composition', 'main', 'shell'].includes(targetZone.kind)) { violations.push(`${fileRelative}: Desktop adapter imports an outer or legacy implementation: ${dependency}`); } } } } function validateStrictZone({ fileRelative, analysis, violations }) { const zone = zoneFor(fileRelative); if (['application', 'bootstrap', 'composition', 'feature', 'shell'].includes(zone.kind)) { if (Object.keys(analysis.bridgePaths).length > 0) { violations.push(`${fileRelative}: ${zone.kind} code accesses the Desktop global bridge`); } if (Object.keys(analysis.environmentCapabilities).some((path) => path.endsWith('.*'))) { violations.push(`${fileRelative}: ${zone.kind} code contains non-static global environment access`); } } if (['bootstrap', 'composition', 'platform', 'shell'].includes(zone.kind)) { if (Object.keys(analysis.hookCalls).length > 0) { violations.push(`${fileRelative}: ${zone.kind} code owns stateful React hooks`); } if (Object.keys(analysis.lifecycleMethods).length > 0) { violations.push(`${fileRelative}: ${zone.kind} code owns React class lifecycle methods`); } const environmentPaths = Object.keys(analysis.environmentCapabilities); const bootstrapEnvironmentIsOnlyMountLookup = zone.kind === 'bootstrap' && environmentPaths.every((path) => ['document.getElementById', 'document.querySelector'].includes(path)); if (zone.kind !== 'platform' && environmentPaths.length > 0 && !bootstrapEnvironmentIsOnlyMountLookup) { violations.push(`${fileRelative}: ${zone.kind} code directly accesses browser environment capabilities`); } } if (zone.kind === 'platform' && (/\.(?:(?:c|m)?(?:jsx|tsx))$/u.test(fileRelative) || analysis.dependencies.includes('react'))) { violations.push(`${fileRelative}: Desktop adapters cannot own React UI`); } if (['application', 'bootstrap', 'composition', 'feature', 'platform', 'shell'].includes(zone.kind) && analysis.unresolvedDependencies > 0) { violations.push(`${fileRelative}: ${zone.kind} code contains a non-static import or require`); } } function validateMetric(path, metric, actual, expected, violations) { if (metric === 'actionFactories') { if (JSON.stringify(actual) !== JSON.stringify(expected)) { violations.push(`${path}: ${metric} changed; expected ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}`); } return; } if (typeof expected === 'number') { if (actual !== expected) violations.push(`${path}: ${metric} changed; expected ${expected}, received ${actual}`); return; } if (!sameJson(actual, expected)) { violations.push(`${path}: ${metric} changed; expected ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}`); } } function validateDebtFile(desktopRoot, path, expected, violations, metrics = ROOT_DEBT_METRICS) { const absolutePath = resolve(desktopRoot, path); if (!existsSync(absolutePath)) { violations.push(`${path}: debt ledger entry points to a missing file`); return; } const analysis = analyzeRendererSource(readFileSync(absolutePath, 'utf8'), path); for (const metric of metrics) { validateMetric(path, metric, analysis[metric], expected[metric], violations); } } function validateLegacyLedger(desktopRoot, config, violations) { const rendererRoot = resolve(desktopRoot, 'src/renderer'); const actualLegacyFiles = sourceFiles(rendererRoot) .map((path) => normalizePath(relative(desktopRoot, path))) .filter((path) => zoneFor(path).kind === 'legacy') .sort(); const expectedLegacyFiles = [...config.legacyRendererFiles].sort(); if (JSON.stringify(actualLegacyFiles) !== JSON.stringify(expectedLegacyFiles)) { violations.push( `unclassified legacy renderer file set changed; expected ${JSON.stringify(expectedLegacyFiles)}, received ${JSON.stringify(actualLegacyFiles)}`, ); } const actualFiles = readdirSync(rendererRoot, { withFileTypes: true }) .filter((entry) => entry.isFile() && LEGACY_APP_SHELL_FILE.test(entry.name)) .map((entry) => `src/renderer/${entry.name}`) .sort(); const expectedFiles = Object.keys(config.legacyAppShell.files).sort(); if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) { violations.push(`legacy AppShell file set changed; expected ${JSON.stringify(expectedFiles)}, received ${JSON.stringify(actualFiles)}`); } for (const [path, expected] of Object.entries(config.legacyAppShell.files)) { validateDebtFile(desktopRoot, path, expected, violations); } const actualClosure = collectRootDependencyClosure(desktopRoot, expectedFiles, violations, 'AppShell'); const expectedClosure = Object.keys(config.legacyAppShell.closure).sort(); if (JSON.stringify(actualClosure) !== JSON.stringify(expectedClosure)) { violations.push( `legacy AppShell transitive renderer closure changed; expected ${JSON.stringify(expectedClosure)}, received ${JSON.stringify(actualClosure)}`, ); } for (const [path, expected] of Object.entries(config.legacyAppShell.closure)) { if (config.legacyAppShell.files[path]) { violations.push(`${path}: AppShell root file must not also appear in its transitive closure`); continue; } if (!isRootClosureDebtSource(path) || DECLARATION_FILE.test(path)) { violations.push(`${path}: AppShell closure debt must point to a non-owner Desktop source`); } validateDebtFile(desktopRoot, path, expected, violations, CAPABILITY_DEBT_METRICS); } for (const [path, expected] of Object.entries(config.rootDebt)) { validateDebtFile(desktopRoot, path, expected, violations); } const appShellDebtPaths = new Set([...expectedFiles, ...expectedClosure]); const rootDebtPaths = Object.keys(config.rootDebt).sort(); const actualRootClosure = collectRootDependencyClosure(desktopRoot, rootDebtPaths, violations, 'renderer root') .filter((path) => !appShellDebtPaths.has(path)); const expectedRootClosure = Object.keys(config.rootDebtClosure).sort(); if (JSON.stringify(actualRootClosure) !== JSON.stringify(expectedRootClosure)) { violations.push( `renderer root transitive legacy closure changed; expected ${JSON.stringify(expectedRootClosure)}, received ${JSON.stringify(actualRootClosure)}`, ); } for (const [path, expected] of Object.entries(config.rootDebtClosure)) { if (config.rootDebt[path] || appShellDebtPaths.has(path)) { violations.push(`${path}: renderer root closure debt overlaps another root debt section`); continue; } if (!isRootClosureDebtSource(path) || DECLARATION_FILE.test(path)) { violations.push(`${path}: renderer root closure debt must point to a non-owner Desktop source`); } validateDebtFile(desktopRoot, path, expected, violations, CAPABILITY_DEBT_METRICS); } const ownedPaths = new Map(); for (const owner of config.ownership) { if (!owner.capability || !owner.targetZone || !Array.isArray(owner.legacyPaths)) { violations.push('ownership entries require capability, targetZone, and legacyPaths'); continue; } for (const path of owner.legacyPaths) { if (ownedPaths.has(path)) { violations.push(`${path}: assigned to both ${ownedPaths.get(path)} and ${owner.capability}`); } ownedPaths.set(path, owner.capability); } } const expectedOwnedPaths = [...expectedFiles, ...Object.keys(config.rootDebt)]; for (const path of expectedOwnedPaths) { if (!ownedPaths.has(path)) violations.push(`${path}: missing target owner in the architecture ledger`); } for (const path of ownedPaths.keys()) { if (!config.legacyAppShell.files[path] && !config.rootDebt[path]) { violations.push(`${path}: ownership ledger path is not recorded renderer root debt`); } } } function htmlAttribute(attributes, name) { const match = attributes.match( new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, 'iu'), ); return match?.[1] ?? match?.[2]; } function parseContractSource(desktopRoot, path, violations) { const absolutePath = resolve(desktopRoot, path); if (!existsSync(absolutePath)) { violations.push(`${path}: renderer entry contract source is missing`); return undefined; } try { return parse(readFileSync(absolutePath, 'utf8'), { createImportExpressions: true, errorRecovery: false, plugins: PARSER_PLUGINS.filter((plugin) => plugin !== 'jsx'), sourceFilename: path, sourceType: 'module', }).program; } catch (error) { violations.push( `${path}: could not parse renderer entry contract: ${error instanceof Error ? error.message : String(error)}`, ); return undefined; } } function nodesIn(program) { if (!program) return []; const nodes = []; function visit(node) { nodes.push(node); for (const child of childNodes(node)) visit(child); } visit(program); return nodes; } function objectPropertyValues(object, name) { if (object?.type !== 'ObjectExpression') return []; return object.properties .filter( (property) => property.type === 'ObjectProperty' && !property.computed && memberName(property.key) === name, ) .map((property) => unwrapExpression(property.value)); } function isIdentifier(node, name) { return unwrapExpression(node)?.type === 'Identifier' && unwrapExpression(node).name === name; } function hasNamedImport(program, source, importedName, localName = importedName) { return program.body.some( (node) => node.type === 'ImportDeclaration' && staticString(node.source) === source && node.specifiers.some( (specifier) => specifier.type === 'ImportSpecifier' && memberName(specifier.imported) === importedName && specifier.local.name === localName, ), ); } function hasDefaultImport(program, source, localName) { return program.body.some( (node) => node.type === 'ImportDeclaration' && staticString(node.source) === source && node.specifiers.some( (specifier) => specifier.type === 'ImportDefaultSpecifier' && specifier.local.name === localName, ), ); } function isNamedMember(node, objectName, propertyName) { const expression = unwrapExpression(node); return ( isMemberExpression(expression) && isIdentifier(expression.object, objectName) && memberPropertyName(expression) === propertyName ); } function isMemberCall(node, objectName, methodName, argumentObject, argumentProperty) { const expression = unwrapExpression(node); return ( expression?.type === 'CallExpression' && isMemberExpression(expression.callee) && isIdentifier(expression.callee.object, objectName) && memberPropertyName(expression.callee) === methodName && expression.arguments.length === 1 && isNamedMember(expression.arguments[0], argumentObject, argumentProperty) ); } function isRendererEntryPathInitializer(node) { const expression = unwrapExpression(node); return ( expression?.type === 'CallExpression' && isIdentifier(expression.callee, 'join') && expression.arguments.length === 5 && isIdentifier(expression.arguments[0], 'mainModuleDirectory') && JSON.stringify(expression.arguments.slice(1).map(staticString)) === JSON.stringify(['..', '..', 'dist-renderer', 'index.html']) ); } function isRendererEntryUrlInitializer(node) { const expression = unwrapExpression(node); if (expression?.type !== 'LogicalExpression' || expression.operator !== '??') return false; const left = unwrapExpression(expression.left); const right = unwrapExpression(expression.right); const viteDevServerUrl = isIdentifier(left, 'viteDevServerUrl'); const fileUrl = isMemberExpression(right) && memberPropertyName(right) === 'href' && unwrapExpression(right.object)?.type === 'CallExpression' && isIdentifier(unwrapExpression(right.object).callee, 'pathToFileURL') && unwrapExpression(right.object).arguments.length === 1 && isIdentifier(unwrapExpression(right.object).arguments[0], 'rendererEntryPath'); return viteDevServerUrl && fileUrl; } function isViteDevServerFlag(node) { const expression = unwrapExpression(node); const inner = expression?.type === 'UnaryExpression' && expression.operator === '!' ? unwrapExpression(expression.argument) : undefined; return ( inner?.type === 'UnaryExpression' && inner.operator === '!' && isIdentifier(inner.argument, 'viteDevServerUrl') ); } function frozenRendererEntryObject(node) { const expression = unwrapExpression(node); if ( expression?.type !== 'CallExpression' || expression.arguments.length !== 1 || !isMemberExpression(expression.callee) || expression.callee.computed || !isIdentifier(expression.callee.object, 'Object') || memberPropertyName(expression.callee) !== 'freeze' ) { return undefined; } const object = unwrapExpression(expression.arguments[0]); return object?.type === 'ObjectExpression' ? object : undefined; } function expressionRootIdentifier(node) { let expression = unwrapExpression(node); while (isMemberExpression(expression)) expression = unwrapExpression(expression.object); return expression?.type === 'Identifier' ? expression.name : undefined; } function mutatesIdentifier(node, name) { if (node.type === 'AssignmentExpression') return expressionRootIdentifier(node.left) === name; if (node.type === 'UpdateExpression') return expressionRootIdentifier(node.argument) === name; return ( node.type === 'UnaryExpression' && node.operator === 'delete' && expressionRootIdentifier(node.argument) === name ); } function navigationApiToken(node) { if (node?.type === 'Identifier' && ['loadFile', 'loadURL'].includes(node.name)) return node.name; const value = staticString(node); if (['loadFile', 'loadURL'].includes(value ?? '')) return value; return undefined; } function isOnlyAwaitedMemberCall(block, objectName, methodName, argumentObject, argumentProperty) { if (block?.type !== 'BlockStatement' || block.body.length !== 1) return false; const [statement] = block.body; const expression = statement?.type === 'ExpressionStatement' ? unwrapExpression(statement.expression) : undefined; return ( expression?.type === 'AwaitExpression' && isMemberCall(expression.argument, objectName, methodName, argumentObject, argumentProperty) ); } function isImportMetaDirectory(node) { const expression = unwrapExpression(node); return ( isMemberExpression(expression) && unwrapExpression(expression.object)?.type === 'MetaProperty' && unwrapExpression(expression.object).meta?.name === 'import' && unwrapExpression(expression.object).property?.name === 'meta' && memberPropertyName(expression) === 'dirname' ); } function isProcessViteDevServerUrl(node) { const expression = unwrapExpression(node); return ( isMemberExpression(expression) && isMemberExpression(expression.object) && isIdentifier(expression.object.object, 'process') && memberPropertyName(expression.object) === 'env' && memberPropertyName(expression) === 'VITE_DEV_SERVER_URL' ); } function validateMainRendererLoader(desktopRoot, violations) { const program = parseContractSource(desktopRoot, MAIN_RENDERER_LOADER_SOURCE, violations); if (!program) return; const nodes = nodesIn(program); const declarators = (name) => nodes.filter( (node) => node.type === 'VariableDeclarator' && node.id?.type === 'Identifier' && node.id.name === name, ); const entryPaths = declarators('rendererEntryPath'); const entryUrls = declarators('rendererEntryUrl'); const loadCalls = nodes.filter( (node) => node.type === 'CallExpression' && isMemberExpression(node.callee) && ['loadFile', 'loadURL'].includes(memberPropertyName(node.callee)), ); const loaderFunctions = nodes.filter( (node) => node.type === 'FunctionDeclaration' && node.id?.name === 'loadMainRenderer', ); const resolverFunctions = nodes.filter( (node) => node.type === 'FunctionDeclaration' && node.id?.name === 'resolveMainRendererEntry', ); const [loaderFunction] = loaderFunctions; const [resolverFunction] = resolverFunctions; const ifStatements = loaderFunction ? nodesIn(loaderFunction.body).filter((node) => node.type === 'IfStatement') : []; const [loadBranch] = ifStatements; const resolverReturns = resolverFunction ? nodesIn(resolverFunction.body).filter((node) => node.type === 'ReturnStatement') : []; const [resolverReturn] = resolverReturns; const frozenEntry = frozenRendererEntryObject(resolverReturn?.argument); const returnFilePaths = objectPropertyValues(frozenEntry, 'filePath'); const returnUrls = objectPropertyValues(frozenEntry, 'url'); const returnDevFlags = objectPropertyValues(frozenEntry, 'useDevServer'); const navigationTokens = nodes.filter((node) => navigationApiToken(node)); const valid = hasNamedImport(program, 'node:path', 'join') && hasNamedImport(program, 'node:url', 'pathToFileURL') && !program.body.some((statement) => statementBindings(statement).includes('Object')) && resolverFunctions.length === 1 && resolverFunction.async !== true && JSON.stringify(resolverFunction.params.map((parameter) => parameter.type === 'Identifier' ? parameter.name : undefined)) === JSON.stringify(['mainModuleDirectory', 'viteDevServerUrl']) && resolverFunction.body.body.length === 3 && resolverReturns.length === 1 && Boolean(frozenEntry) && returnFilePaths.length === 1 && isIdentifier(returnFilePaths[0], 'rendererEntryPath') && returnUrls.length === 1 && isIdentifier(returnUrls[0], 'rendererEntryUrl') && returnDevFlags.length === 1 && isViteDevServerFlag(returnDevFlags[0]) && loaderFunctions.length === 1 && loaderFunction.async === true && JSON.stringify(loaderFunction.params.map((parameter) => parameter.type === 'Identifier' ? parameter.name : undefined)) === JSON.stringify(['mainWindow', 'rendererEntry']) && loaderFunction.body.body.length === 1 && entryPaths.length === 1 && isRendererEntryPathInitializer(entryPaths[0].init) && entryUrls.length === 1 && isRendererEntryUrlInitializer(entryUrls[0].init) && loadCalls.length === 2 && loadCalls.filter((node) => isMemberCall(node, 'mainWindow', 'loadFile', 'rendererEntry', 'filePath')).length === 1 && loadCalls.filter((node) => isMemberCall(node, 'mainWindow', 'loadURL', 'rendererEntry', 'url')).length === 1 && navigationTokens.length === 4 && ifStatements.length === 1 && isNamedMember(loadBranch.test, 'rendererEntry', 'useDevServer') && isOnlyAwaitedMemberCall(loadBranch.consequent, 'mainWindow', 'loadURL', 'rendererEntry', 'url') && isOnlyAwaitedMemberCall(loadBranch.alternate, 'mainWindow', 'loadFile', 'rendererEntry', 'filePath'); if (!valid) { violations.push( `${MAIN_RENDERER_LOADER_SOURCE}: renderer loader must load only the pinned dist-renderer/index.html entry`, ); } } function validateMainWindowEntryContract(desktopRoot, violations) { const program = parseContractSource(desktopRoot, RENDERER_LOADER_SOURCE, violations); if (!program) return; const nodes = nodesIn(program); const parents = buildParentMap(program); const loaderImports = program.body.filter( (node) => node.type === 'ImportDeclaration' && staticString(node.source) === './main-renderer-loader.js' && ['loadMainRenderer', 'resolveMainRendererEntry'].every((name) => node.specifiers.some( (specifier) => specifier.type === 'ImportSpecifier' && memberName(specifier.imported) === name && specifier.local.name === name, ), ), ); const entryDeclarators = nodes.filter( (node) => node.type === 'VariableDeclarator' && node.id?.type === 'Identifier' && node.id.name === 'rendererEntry' && unwrapExpression(node.init)?.type === 'CallExpression' && isIdentifier(unwrapExpression(node.init).callee, 'resolveMainRendererEntry') && unwrapExpression(node.init).arguments.length === 2 && isImportMetaDirectory(unwrapExpression(node.init).arguments[0]) && isProcessViteDevServerUrl(unwrapExpression(node.init).arguments[1]), ); const loaderCalls = nodes.filter( (node) => node.type === 'CallExpression' && isIdentifier(node.callee, 'loadMainRenderer') && node.arguments.length === 2 && isIdentifier(node.arguments[0], 'mainWindow') && isIdentifier(node.arguments[1], 'rendererEntry'), ); const entryDeclaration = entryDeclarators.length === 1 ? parents.get(entryDeclarators[0]) : undefined; const entryMutations = nodes.filter((node) => mutatesIdentifier(node, 'rendererEntry')); if ( loaderImports.length !== 1 || entryDeclarators.length !== 1 || entryDeclaration?.type !== 'VariableDeclaration' || entryDeclaration.kind !== 'const' || loaderCalls.length !== 1 || entryMutations.length > 0 || nodes.some((node) => navigationApiToken(node)) ) { violations.push( `${RENDERER_LOADER_SOURCE}: main window must delegate exactly once to the pinned renderer loader`, ); } const allowedNavigationFiles = new Set([ 'src/main/browser/controller.ts', 'src/main/computer-use/cursor-overlay-window.ts', 'src/main/computer-use/pip-electron.ts', MAIN_RENDERER_LOADER_SOURCE, 'src/main/permission-overlay/permission-overlay-main.ts', ]); for (const file of sourceFiles(resolve(desktopRoot, 'src/main'))) { const fileRelative = normalizePath(relative(desktopRoot, file)); if (isTestConsumer(fileRelative) || allowedNavigationFiles.has(fileRelative)) continue; const sourceProgram = parseContractSource(desktopRoot, fileRelative, violations); if (sourceProgram && nodesIn(sourceProgram).some((node) => navigationApiToken(node))) { violations.push(`${fileRelative}: main-process navigation APIs are restricted to approved loaders`); } } validateMainRendererLoader(desktopRoot, violations); } function validateViteEntryContract(desktopRoot, violations) { const program = parseContractSource(desktopRoot, RENDERER_VITE_CONFIG, violations); if (!program) return; const defaultExports = program.body.filter((node) => node.type === 'ExportDefaultDeclaration'); const declaration = unwrapExpression(defaultExports[0]?.declaration); const config = defaultExports.length === 1 && declaration?.type === 'CallExpression' && isIdentifier(declaration.callee, 'defineConfig') && declaration.arguments.length === 1 ? unwrapExpression(declaration.arguments[0]) : undefined; const roots = objectPropertyValues(config, 'root'); const builds = objectPropertyValues(config, 'build'); const plugins = objectPropertyValues(config, 'plugins'); const outDirs = objectPropertyValues(builds[0], 'outDir'); const rollupOptions = objectPropertyValues(builds[0], 'rollupOptions'); const hasInputOverride = rollupOptions.some( (options) => options?.type !== 'ObjectExpression' || objectPropertyValues(options, 'input').length > 0, ); const hasContractSpread = config?.type !== 'ObjectExpression' || config.properties.some((property) => property.type === 'SpreadElement') || builds[0]?.type !== 'ObjectExpression' || builds[0].properties.some((property) => property.type === 'SpreadElement'); const pluginElements = plugins[0]?.type === 'ArrayExpression' ? plugins[0].elements : []; const pluginCall = (index, name, argumentCount) => { const expression = unwrapExpression(pluginElements[index]); return expression?.type === 'CallExpression' && isIdentifier(expression.callee, name) && expression.arguments.length === argumentCount ? expression : undefined; }; const reactCall = pluginCall(0, 'react', 0); const dependencyPatchesCall = pluginCall(1, 'dependencyPatchesCachePlugin', 1); const bundledPackagesCall = pluginCall(2, 'bundledNpmPackagesPlugin', 0); const rendererContractCall = pluginCall(3, 'rendererEntryContractPlugin', 1); const rendererContractRoot = unwrapExpression(rendererContractCall?.arguments[0]); const hasPinnedRendererContractRoot = rendererContractRoot?.type === 'CallExpression' && isIdentifier(rendererContractRoot.callee, 'resolve') && rendererContractRoot.arguments.length === 2 && isImportMetaDirectory(rendererContractRoot.arguments[0]) && staticString(rendererContractRoot.arguments[1]) === 'src/renderer'; const hasPinnedImports = hasNamedImport(program, 'vite', 'defineConfig') && hasNamedImport(program, 'node:path', 'resolve') && hasDefaultImport(program, '@vitejs/plugin-react', 'react') && hasNamedImport(program, './vite-dependency-patches.js', 'dependencyPatchesCachePlugin') && hasNamedImport(program, './vite-bundled-packages.js', 'bundledNpmPackagesPlugin') && hasNamedImport( program, './scripts/vite-renderer-entry-contract.js', 'rendererEntryContractPlugin', ); const hasPinnedPlugins = pluginElements.length === 4 && Boolean(reactCall) && Boolean(dependencyPatchesCall) && isIdentifier(dependencyPatchesCall.arguments[0], 'REPO_ROOT') && Boolean(bundledPackagesCall) && Boolean(rendererContractCall) && hasPinnedRendererContractRoot; if ( roots.length !== 1 || staticString(roots[0]) !== 'src/renderer' || builds.length !== 1 || outDirs.length !== 1 || staticString(outDirs[0]) !== '../../dist-renderer' || plugins.length !== 1 || !hasPinnedImports || !hasPinnedPlugins || hasInputOverride || hasContractSpread ) { violations.push( `${RENDERER_VITE_CONFIG}: Vite must build src/renderer/index.html into dist-renderer without an input override`, ); } const packagePath = resolve(desktopRoot, 'package.json'); let buildScript; try { buildScript = JSON.parse(readFileSync(packagePath, 'utf8')).scripts?.['build:renderer']; } catch { buildScript = undefined; } if (buildScript !== RENDERER_BUILD_SCRIPT) { violations.push('package.json: build:renderer must retain the pinned Vite renderer entry command'); } } function validateRendererEntryContract(desktopRoot, config, violations) { const indexPath = resolve(desktopRoot, RENDERER_ENTRY_INDEX); const entryPath = resolve(desktopRoot, RENDERER_ENTRY_SOURCE); if (!existsSync(indexPath)) { violations.push(`${RENDERER_ENTRY_INDEX}: renderer entry index is missing`); return; } if (!existsSync(entryPath)) { violations.push(`${RENDERER_ENTRY_SOURCE}: renderer entry source is missing`); } if (!Object.hasOwn(config.rootDebt, RENDERER_ENTRY_SOURCE)) { violations.push(`${RENDERER_ENTRY_SOURCE}: renderer entry must retain its permanent rootDebt guard`); } const html = readFileSync(indexPath, 'utf8'); const openingScriptCount = html.match(/]*)>([\s\S]*?)<\/script\s*>/giu)]; const [script] = scripts; const isPinnedEntry = openingScriptCount === 1 && scripts.length === 1 && htmlAttribute(script?.[1] ?? '', 'type') === 'module' && htmlAttribute(script?.[1] ?? '', 'src') === RENDERER_ENTRY_SCRIPT_SOURCE && (script?.[2] ?? '').trim().length === 0; if (!isPinnedEntry) { violations.push( `${RENDERER_ENTRY_INDEX}: renderer entry contract requires exactly one empty external module script for ${RENDERER_ENTRY_SCRIPT_SOURCE}`, ); } validateMainWindowEntryContract(desktopRoot, violations); validateViteEntryContract(desktopRoot, violations); } function metricTotal(value) { if (typeof value === 'number') return value; if (Array.isArray(value)) return value.length; return Object.values(value).reduce((total, count) => total + count, 0); } function allowsMigrationDependency({ base, current, dependency, desktopRoot, path, section }) { if (metricTotal(current.dependencyPaths) > metricTotal(base.dependencyPaths)) return false; const target = resolveDependency(desktopRoot, resolve(desktopRoot, path), dependency); if (!target) return false; const targetRelative = normalizePath(relative(desktopRoot, target)); const targetZone = zoneFor(targetRelative); if (section === 'legacyAppShell' || section === 'legacyAppShellClosure') { if (targetZone.kind === 'shell' || isPublicApplicationPath(targetRelative)) return true; const targetFeature = featureForAbsolutePath(desktopRoot, target); return Boolean(targetFeature && isPublicFeaturePath(targetFeature.subpath)); } if (section === 'rootDebt') return ['bootstrap', 'composition'].includes(targetZone.kind); return ['application', 'bootstrap', 'composition', 'platform'].includes(targetZone.kind); } function debtForPath(desktopRoot, path) { return debtMetrics(analyzeRendererSource(readFileSync(resolve(desktopRoot, path), 'utf8'), path)); } function capabilityDebtForPath(desktopRoot, path) { return capabilityDebtMetrics(analyzeRendererSource(readFileSync(resolve(desktopRoot, path), 'utf8'), path)); } function collectLegacyImportEdges(desktopRoot) { const feature = new Set(); const platform = new Set(); for (const file of sourceFiles(resolve(desktopRoot, 'src'))) { const fileRelative = normalizePath(relative(desktopRoot, file)); const sourceZone = zoneFor(fileRelative); if (sourceZone.kind !== 'feature' && sourceZone.kind !== 'platform') continue; const analysis = analyzeRendererSource(readFileSync(file, 'utf8'), fileRelative); for (const dependency of analysis.dependencies) { const target = resolveDependency(desktopRoot, file, dependency); if (!target) continue; const targetRelative = normalizePath(relative(desktopRoot, target)); if (zoneFor(targetRelative).kind !== 'legacy') continue; const edge = `${fileRelative} -> ${targetRelative}`; if (sourceZone.kind === 'feature') feature.add(edge); else platform.add(edge); } } return { feature: [...feature].sort(), platform: [...platform].sort() }; } export function generateArchitectureConfig(desktopRoot, config) { const rendererRoot = resolve(desktopRoot, 'src/renderer'); const legacyRendererFiles = sourceFiles(rendererRoot) .map((path) => normalizePath(relative(desktopRoot, path))) .filter((path) => zoneFor(path).kind === 'legacy') .sort(); const appShellFiles = readdirSync(rendererRoot, { withFileTypes: true }) .filter((entry) => entry.isFile() && LEGACY_APP_SHELL_FILE.test(entry.name)) .map((entry) => `src/renderer/${entry.name}`) .sort(); const closureViolations = []; const closureFiles = collectRootDependencyClosure(desktopRoot, appShellFiles, closureViolations, 'AppShell'); if (closureViolations.length > 0) { throw new Error(`cannot generate AppShell closure:\n${closureViolations.join('\n')}`); } const imports = collectLegacyImportEdges(desktopRoot); const rootDebt = {}; for (const path of Object.keys(config.rootDebt ?? {}).sort()) { if (existsSync(resolve(desktopRoot, path))) rootDebt[path] = debtForPath(desktopRoot, path); } const appShellDebtPaths = new Set([...appShellFiles, ...closureFiles]); const rootDebtClosureFiles = collectRootDependencyClosure( desktopRoot, Object.keys(rootDebt), closureViolations, 'renderer root', ).filter((path) => !appShellDebtPaths.has(path)); if (closureViolations.length > 0) { throw new Error(`cannot generate renderer root closure:\n${closureViolations.join('\n')}`); } return { version: 1, legacyRendererFiles, legacyGrowthDirectories: config.legacyGrowthDirectories ?? DEFAULT_LEGACY_GROWTH_DIRECTORIES, legacyFeatureImports: imports.feature, legacyPlatformImports: imports.platform, legacyAppShell: { files: Object.fromEntries(appShellFiles.map((path) => [path, debtForPath(desktopRoot, path)])), closure: Object.fromEntries(closureFiles.map((path) => [path, capabilityDebtForPath(desktopRoot, path)])), }, rootDebt, rootDebtClosure: Object.fromEntries( rootDebtClosureFiles.map((path) => [path, capabilityDebtForPath(desktopRoot, path)]), ), ownership: config.ownership ?? [], }; } function validateMonotonicDebt(config, baseConfig, desktopRoot, violations) { if (!baseConfig) return; for (const section of ['legacyAppShell', 'legacyAppShellClosure', 'rootDebt', 'rootDebtClosure']) { const currentFiles = section === 'legacyAppShell' ? config.legacyAppShell.files : section === 'legacyAppShellClosure' ? config.legacyAppShell.closure : section === 'rootDebt' ? config.rootDebt : config.rootDebtClosure; const baseFiles = section === 'legacyAppShell' ? baseConfig.legacyAppShell.files : section === 'legacyAppShellClosure' ? baseConfig.legacyAppShell.closure : section === 'rootDebt' ? baseConfig.rootDebt : baseConfig.rootDebtClosure; for (const path of Object.keys(currentFiles)) { const shiftedAppShellBase = section === 'rootDebtClosure' ? baseConfig.legacyAppShell.closure[path] : undefined; if (!baseFiles[path] && !shiftedAppShellBase) { violations.push(`${path}: new ${section} debt entries are forbidden`); } } if (section === 'rootDebt') { for (const path of Object.keys(baseFiles)) { if (!currentFiles[path] && existsSync(resolve(desktopRoot, path))) { violations.push(`${path}: permanent root entry guard cannot be removed while the source exists`); } } } for (const [path, current] of Object.entries(currentFiles)) { const base = baseFiles[path] ?? (section === 'rootDebtClosure' ? baseConfig.legacyAppShell.closure[path] : undefined); if (!base) continue; const metrics = section.endsWith('Closure') ? CAPABILITY_DEBT_METRICS : ROOT_DEBT_METRICS; for (const metric of metrics) { if (metricTotal(current[metric]) > metricTotal(base[metric])) { violations.push(`${path}: ${metric} debt increased from ${metricTotal(base[metric])} to ${metricTotal(current[metric])}`); } if (['bridgePaths', 'environmentCapabilities', 'hookCalls', 'lifecycleMethods'].includes(metric)) { const increases = Object.fromEntries( Object.entries(current[metric]) .map(([key, count]) => [key, Math.max(0, count - (base[metric][key] ?? 0))]) .filter(([, count]) => count > 0), ); for (const [key, count] of Object.entries(increases)) { if (count > 0) { violations.push(`${path}: new or increased ${metric} debt ${key}`); } } } if (metric === 'actionFactories') { for (const factory of current.actionFactories) { if (!base.actionFactories.includes(factory)) violations.push(`${path}: new action factory debt ${factory}`); } } if (metric === 'dependencyPaths') { for (const [dependency, count] of Object.entries(current.dependencyPaths)) { if ( count > (base.dependencyPaths[dependency] ?? 0) && !allowsMigrationDependency({ base, current, dependency, desktopRoot, path, section }) ) { violations.push(`${path}: new dependency debt ${dependency}`); } } } } } } const baseLegacyFiles = new Set(baseConfig.legacyRendererFiles); for (const path of config.legacyRendererFiles) { if (!baseLegacyFiles.has(path) && !isAllowedLegacyGrowthPath(config, path)) { violations.push(`${path}: new unclassified renderer source files are forbidden outside approved legacy directories`); } } const baseLegacyGrowthDirectories = new Set(baseConfig.legacyGrowthDirectories); for (const path of config.legacyGrowthDirectories) { if (!baseLegacyGrowthDirectories.has(path)) { violations.push(`${path}: new legacy growth directories are forbidden`); } } const baseLegacyFeatureImports = new Set(baseConfig.legacyFeatureImports); for (const edge of config.legacyFeatureImports) { if (!baseLegacyFeatureImports.has(edge)) violations.push(`${edge}: new feature-to-legacy imports are forbidden`); } const baseLegacyPlatformImports = new Set(baseConfig.legacyPlatformImports); for (const edge of config.legacyPlatformImports) { if (!baseLegacyPlatformImports.has(edge)) violations.push(`${edge}: new platform-to-legacy imports are forbidden`); } } export function checkRendererArchitecture({ desktopRoot, config, baseConfig, enforceRendererEntryContract = true, } = {}) { const resolvedDesktopRoot = resolve(desktopRoot ?? fileURLToPath(new URL('..', import.meta.url))); const resolvedConfig = config ?? JSON.parse(readFileSync(join(resolvedDesktopRoot, 'renderer-architecture.json'), 'utf8')); const violations = []; const currentConfigValid = validateArchitectureConfig(resolvedConfig, 'current', violations); const baseConfigValid = baseConfig ? validateArchitectureConfig(baseConfig, 'base', violations) : true; if (!currentConfigValid || !baseConfigValid) return violations.sort(); if (enforceRendererEntryContract) { validateRendererEntryContract(resolvedDesktopRoot, resolvedConfig, violations); } validateLegacyLedger(resolvedDesktopRoot, resolvedConfig, violations); validateMonotonicDebt(resolvedConfig, baseConfig, resolvedDesktopRoot, violations); const allowedLegacyFeatureImports = new Set(resolvedConfig.legacyFeatureImports); const allowedLegacyPlatformImports = new Set(resolvedConfig.legacyPlatformImports); const observedLegacyFeatureImports = new Set(); const observedLegacyPlatformImports = new Set(); for (const scanRoot of ['src', 'stories', 'e2e']) { for (const file of sourceFiles(resolve(resolvedDesktopRoot, scanRoot))) { const fileRelative = normalizePath(relative(resolvedDesktopRoot, file)); let analysis; try { analysis = analyzeRendererSource(readFileSync(file, 'utf8'), fileRelative); } catch (error) { violations.push(`${fileRelative}: could not parse source: ${error instanceof Error ? error.message : String(error)}`); continue; } validateStrictZone({ fileRelative, analysis, violations }); validateDependencies({ allowedLegacyFeatureImports, allowedLegacyPlatformImports, analysis, desktopRoot: resolvedDesktopRoot, file, fileRelative, observedLegacyFeatureImports, observedLegacyPlatformImports, violations, }); } } for (const edge of allowedLegacyFeatureImports) { if (!observedLegacyFeatureImports.has(edge)) violations.push(`${edge}: stale feature-to-legacy import budget`); } for (const edge of allowedLegacyPlatformImports) { if (!observedLegacyPlatformImports.has(edge)) violations.push(`${edge}: stale platform-to-legacy import budget`); } return violations.sort(); } // The monotonic-debt ratchet must measure debt against what the base commit's // source tree *actually* contained, not against the numbers its ledger happened // to record. A ledger that under-reports its own tree (for example, one // generated on a branch that predated files already merged into main) would // otherwise make a faithful baseline correction look like brand-new debt and // wedge the ledger permanently. We materialize the base tree and re-derive its // debt, keeping the base ledger only as the source of policy fields (hook // transitions, growth directories, root-debt key set, ownership). function deriveBaseTreeConfig(repoRoot, desktopRoot, base, baseCommittedConfig) { const scratch = mkdtempSync(join(tmpdir(), 'renderer-arch-base-')); const worktreePath = join(scratch, 'tree'); try { execFileSync('git', ['worktree', 'add', '--detach', worktreePath, base], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], }); const baseDesktopRoot = resolve(worktreePath, relative(repoRoot, desktopRoot)); return generateArchitectureConfig(baseDesktopRoot, baseCommittedConfig); } finally { try { execFileSync('git', ['worktree', 'remove', '--force', worktreePath], { cwd: repoRoot, stdio: 'ignore', }); } catch { try { execFileSync('git', ['worktree', 'prune'], { cwd: repoRoot, stdio: 'ignore' }); } catch { // Ignore prune failures; the scratch removal below is the real cleanup. } } try { rmSync(scratch, { recursive: true, force: true }); } catch { // Best-effort cleanup of the scratch directory. } } } function loadBaseConfig(repoRoot, desktopRoot, base) { if (!base) return { baseConfig: undefined, introducedLedger: false }; const relativeConfig = normalizePath(relative(repoRoot, join(desktopRoot, 'renderer-architecture.json'))); try { execFileSync('git', ['rev-parse', '--verify', `${base}^{commit}`], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }); } catch { throw new Error(`base ref does not resolve to a commit: ${base}`); } let source; try { source = execFileSync('git', ['show', `${base}:${relativeConfig}`], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }); } catch { const diffStatus = execFileSync('git', ['diff', '--name-status', base, '--', relativeConfig], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }).trim(); const worktreeStatus = execFileSync( 'git', ['status', '--porcelain=v1', '--untracked-files=all', '--', relativeConfig], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }, ).trim(); if (diffStatus === `A\t${relativeConfig}` || worktreeStatus === `?? ${relativeConfig}`) { return { baseConfig: undefined, introducedLedger: true }; } throw new Error(`base ledger is missing at ${base}:${relativeConfig}`); } let baseCommittedConfig; try { baseCommittedConfig = JSON.parse(source); } catch (error) { throw new Error( `base ledger is invalid JSON at ${base}:${relativeConfig}: ${error instanceof Error ? error.message : String(error)}`, ); } try { return { baseConfig: deriveBaseTreeConfig(repoRoot, desktopRoot, base, baseCommittedConfig), introducedLedger: false, }; } catch (error) { // If the base tree cannot be materialized or analyzed (e.g. git worktree is // unavailable), fall back to the committed base ledger so the ratchet still // runs. This restores the pre-fix behavior rather than crashing the check. console.warn( `Renderer architecture check: could not derive base tree debt at ${base}; ` + `falling back to the committed base ledger. (${error instanceof Error ? error.message : String(error)})`, ); return { baseConfig: baseCommittedConfig, introducedLedger: false }; } } function parseCliArguments(args) { let base; let write = false; for (let index = 0; index < args.length; index += 1) { const argument = args[index]; if (argument === '--write' && !write) { write = true; continue; } if (argument === '--base' && base === undefined) { const value = args[index + 1]; if (!value || value.startsWith('--')) { throw new Error('usage: check-renderer-architecture.mjs [--write] [--base ]'); } base = value; index += 1; continue; } throw new Error('usage: check-renderer-architecture.mjs [--write] [--base ]'); } return { base, write }; } function runCli() { const desktopRoot = resolve(fileURLToPath(new URL('..', import.meta.url))); const repoRoot = resolve(desktopRoot, '../..'); let base; let config; let loadedBase; let write; try { ({ base, write } = parseCliArguments(process.argv.slice(2))); config = JSON.parse(readFileSync(join(desktopRoot, 'renderer-architecture.json'), 'utf8')); loadedBase = loadBaseConfig(repoRoot, desktopRoot, base); if (write) { config = generateArchitectureConfig(desktopRoot, config); writeFileSync(join(desktopRoot, 'renderer-architecture.json'), `${JSON.stringify(config, null, 2)}\n`); console.log('Renderer architecture ledger updated.'); } } catch (error) { console.error(`Renderer architecture check could not start: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; return; } const { baseConfig, introducedLedger } = loadedBase; const violations = checkRendererArchitecture({ baseConfig, config, desktopRoot }); if (violations.length > 0) { console.error('Renderer architecture check failed:'); for (const violation of violations) console.error(`- ${violation}`); process.exitCode = 1; return; } if (introducedLedger) { console.log(`Renderer architecture check passed; ${base} has no ledger and this change introduces it.`); return; } console.log(`Renderer architecture check passed${baseConfig ? ` against ${base}` : ''}.`); } if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) runCli();