diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json index 4086f5f6de..2fc103c086 100644 --- a/apps/desktop/renderer-architecture.json +++ b/apps/desktop/renderer-architecture.json @@ -325,7 +325,6 @@ "legacyPlatformImports": [ "src/renderer/platform/desktop/create-workbar-services.ts -> src/renderer/session-message-settlement" ], - "hookTransitions": [], "legacyAppShell": { "files": { "src/renderer/app-shell-app-update.ts": { diff --git a/apps/desktop/scripts/check-renderer-architecture.mjs b/apps/desktop/scripts/check-renderer-architecture.mjs index 68fe532f16..9e09dbd51b 100644 --- a/apps/desktop/scripts/check-renderer-architecture.mjs +++ b/apps/desktop/scripts/check-renderer-architecture.mjs @@ -40,12 +40,6 @@ const STATEFUL_HOOKS = new Set([ 'useSyncExternalStore', 'useTransition', ]); -const HOOK_TRANSITION_SECTIONS = new Set([ - 'legacyAppShell', - 'legacyAppShellClosure', - 'rootDebt', - 'rootDebtClosure', -]); const REACT_LIFECYCLE_METHODS = new Set([ 'UNSAFE_componentWillMount', 'UNSAFE_componentWillReceiveProps', @@ -201,7 +195,6 @@ function validateArchitectureConfig(config, label, violations) { for (const field of ['legacyFeatureImports', 'legacyPlatformImports']) { if (!isSortedUniqueStrings(config[field])) reject(`${field} must be sorted unique strings`); } - if (!Array.isArray(config.hookTransitions)) reject('hookTransitions must be an array'); if ( !isRecord(config.legacyAppShell) || !isRecord(config.legacyAppShell.files) || @@ -262,49 +255,6 @@ function validateArchitectureConfig(config, label, violations) { reject(`${owner.capability}: legacyPaths must be sorted unique strings`); } } - const transitionIds = new Set(); - let previousTransitionId = ''; - for (const transition of config.hookTransitions) { - if (!isRecord(transition)) { - reject('hookTransitions entries must be objects'); - continue; - } - if ( - typeof transition.id !== 'string' || - !/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(transition.id) - ) { - reject('hookTransitions id must be lowercase kebab-case'); - } else { - if (transitionIds.has(transition.id)) reject(`duplicate hook transition ${transition.id}`); - if (transition.id.localeCompare(previousTransitionId) < 0) { - reject('hookTransitions must be sorted by id'); - } - transitionIds.add(transition.id); - previousTransitionId = transition.id; - } - if (!HOOK_TRANSITION_SECTIONS.has(transition.section)) { - reject(`${String(transition.id)}: unsupported hook transition section ${String(transition.section)}`); - } - if ( - typeof transition.path !== 'string' || - !transition.path.startsWith('src/') || - transition.path.includes('..') || - transition.path.includes('\\') - ) { - reject(`${String(transition.id)}: hook transition path must be a normalized Desktop source path`); - } - for (const field of ['from', 'to']) { - if (!isTrackedHookName(transition[field])) { - reject(`${String(transition.id)}: hook transition ${field} must be a tracked Hook name`); - } - } - if (transition.from === transition.to) { - reject(`${String(transition.id)}: hook transition must change the Hook name`); - } - if (!Number.isInteger(transition.count) || transition.count <= 0) { - reject(`${String(transition.id)}: hook transition count must be a positive integer`); - } - } return valid; } @@ -424,10 +374,6 @@ function addHookNames(aliases, key, names) { return changed; } -function isTrackedHookName(name) { - return typeof name === 'string' && (name === 'use' || /^use[A-Z0-9]/u.test(name)); -} - function staticString(node) { if (node?.type === 'StringLiteral') return node.value; if (node?.type === 'BinaryExpression' && node.operator === '+') { @@ -2305,7 +2251,6 @@ export function generateArchitectureConfig(desktopRoot, config) { legacyGrowthDirectories: config.legacyGrowthDirectories ?? DEFAULT_LEGACY_GROWTH_DIRECTORIES, legacyFeatureImports: imports.feature, legacyPlatformImports: imports.platform, - hookTransitions: config.hookTransitions ?? [], legacyAppShell: { files: Object.fromEntries(appShellFiles.map((path) => [path, debtForPath(desktopRoot, path)])), closure: Object.fromEntries(closureFiles.map((path) => [path, capabilityDebtForPath(desktopRoot, path)])), @@ -2320,21 +2265,6 @@ export function generateArchitectureConfig(desktopRoot, config) { function validateMonotonicDebt(config, baseConfig, desktopRoot, violations) { if (!baseConfig) return; - const baseTransitions = new Map( - baseConfig.hookTransitions.map((transition) => [transition.id, transition]), - ); - const newTransitions = config.hookTransitions.filter( - (transition) => !baseTransitions.has(transition.id), - ); - for (const [id, baseTransition] of baseTransitions) { - const currentTransition = config.hookTransitions.find((transition) => transition.id === id); - if (!currentTransition) { - violations.push(`${id}: historical hook transition entries cannot be removed`); - } else if (JSON.stringify(currentTransition) !== JSON.stringify(baseTransition)) { - violations.push(`${id}: historical hook transition entries cannot be changed`); - } - } - const consumedTransitions = new Set(); for (const section of ['legacyAppShell', 'legacyAppShellClosure', 'rootDebt', 'rootDebtClosure']) { const currentFiles = section === 'legacyAppShell' @@ -2382,31 +2312,6 @@ function validateMonotonicDebt(config, baseConfig, desktopRoot, violations) { .map(([key, count]) => [key, Math.max(0, count - (base[metric][key] ?? 0))]) .filter(([, count]) => count > 0), ); - if (metric === 'hookCalls') { - const decreases = Object.fromEntries( - Object.entries(base.hookCalls) - .map(([key, count]) => [key, Math.max(0, count - (current.hookCalls[key] ?? 0))]) - .filter(([, count]) => count > 0), - ); - for (const transition of newTransitions) { - if (transition.section !== section || transition.path !== path) continue; - const availableFrom = Object.hasOwn(decreases, transition.from) - ? decreases[transition.from] - : 0; - const requiredTo = Object.hasOwn(increases, transition.to) - ? increases[transition.to] - : 0; - if (availableFrom < transition.count || requiredTo < transition.count) { - violations.push( - `${transition.id}: hook transition must be paid by ${transition.count} removed ${transition.from} and ${transition.count} added ${transition.to}`, - ); - continue; - } - decreases[transition.from] = availableFrom - transition.count; - increases[transition.to] = requiredTo - transition.count; - consumedTransitions.add(transition.id); - } - } for (const [key, count] of Object.entries(increases)) { if (count > 0) { violations.push(`${path}: new or increased ${metric} debt ${key}`); @@ -2431,12 +2336,6 @@ function validateMonotonicDebt(config, baseConfig, desktopRoot, violations) { } } } - for (const transition of newTransitions) { - if (!consumedTransitions.has(transition.id)) { - violations.push(`${transition.id}: new hook transition was not consumed by this change`); - } - } - const baseLegacyFiles = new Set(baseConfig.legacyRendererFiles); for (const path of config.legacyRendererFiles) { if (!baseLegacyFiles.has(path) && !isAllowedLegacyGrowthPath(config, path)) { diff --git a/apps/desktop/scripts/check-renderer-architecture.test.mjs b/apps/desktop/scripts/check-renderer-architecture.test.mjs index b7a71bf6e1..07b50469b8 100644 --- a/apps/desktop/scripts/check-renderer-architecture.test.mjs +++ b/apps/desktop/scripts/check-renderer-architecture.test.mjs @@ -60,7 +60,6 @@ function architectureConfig({ legacyFiles = {}, legacyGrowthDirectories = [], legacyPlatformImports = [], - hookTransitions = [], rootDebt = {}, rootDebtClosure = {}, legacyRendererFiles = Object.keys(rootDebt), @@ -72,7 +71,6 @@ function architectureConfig({ legacyGrowthDirectories: [...legacyGrowthDirectories].sort(), legacyFeatureImports: [...legacyFeatureImports].sort(), legacyPlatformImports: [...legacyPlatformImports].sort(), - hookTransitions: [...hookTransitions].sort((left, right) => left.id.localeCompare(right.id)), legacyAppShell: { files: legacyFiles, closure: legacyAppShellClosureDebt ?? {}, @@ -1056,104 +1054,6 @@ describe('renderer architecture checker fixtures', () => { ); }); - it('allows a one-time Hook replacement paid by removed Hook debt', async () => { - await withDesktopFixture( - transitiveAppShellFiles(` - import { useState } from 'react'; - export function legacySessionHelper() { - return useState('session'); - } - `), - (desktopRoot) => { - const currentConfig = generateArchitectureConfig( - desktopRoot, - transitiveAppShellSeedConfig(), - ); - currentConfig.hookTransitions = [ - { - id: 'replace-session-reducer-with-state-read', - section: 'legacyAppShellClosure', - path: TRANSITIVE_LEGACY_HELPER_PATH, - from: 'useReducer', - to: 'useState', - count: 1, - }, - ]; - const baseConfig = structuredClone(currentConfig); - baseConfig.hookTransitions = []; - baseConfig.legacyAppShell.closure[TRANSITIVE_LEGACY_HELPER_PATH].hookCalls = { - useReducer: 1, - }; - - assert.deepEqual(violationsFor(desktopRoot, currentConfig, baseConfig), []); - }, - ); - }); - - it('rejects unconsumed, underfunded, and reused Hook transitions', async () => { - await withDesktopFixture( - transitiveAppShellFiles(` - import { useState } from 'react'; - export function legacySessionHelper() { - return useState('session'); - } - `), - (desktopRoot) => { - const currentConfig = generateArchitectureConfig( - desktopRoot, - transitiveAppShellSeedConfig(), - ); - const transition = { - id: 'replace-session-reducer-with-state-read', - section: 'legacyAppShellClosure', - path: TRANSITIVE_LEGACY_HELPER_PATH, - from: 'useReducer', - to: 'useState', - count: 2, - }; - currentConfig.hookTransitions = [transition]; - const underfundedBase = structuredClone(currentConfig); - underfundedBase.hookTransitions = []; - underfundedBase.legacyAppShell.closure[TRANSITIVE_LEGACY_HELPER_PATH].hookCalls = { - useReducer: 1, - }; - const underfunded = violationsFor(desktopRoot, currentConfig, underfundedBase); - assertHasViolation( - underfunded, - /replace-session-reducer-with-state-read: hook transition must be paid/u, - ); - assertHasViolation( - underfunded, - /replace-session-reducer-with-state-read: new hook transition was not consumed/u, - ); - - const reusedBase = structuredClone(currentConfig); - reusedBase.legacyAppShell.closure[TRANSITIVE_LEGACY_HELPER_PATH].hookCalls = { - useReducer: 2, - }; - const reused = violationsFor(desktopRoot, currentConfig, reusedBase); - assertHasViolation( - reused, - /legacy-session-helper\.ts: new or increased hookCalls debt useState/u, - ); - - const prototypeKeyConfig = structuredClone(currentConfig); - prototypeKeyConfig.hookTransitions = [ - { ...transition, count: 1, from: 'toString' }, - ]; - const prototypeKeyBase = structuredClone(prototypeKeyConfig); - prototypeKeyBase.hookTransitions = []; - prototypeKeyBase.legacyAppShell.closure[TRANSITIVE_LEGACY_HELPER_PATH].hookCalls = { - useReducer: 1, - }; - assertHasViolation( - violationsFor(desktopRoot, prototypeKeyConfig, prototypeKeyBase), - /hook transition from must be a tracked Hook name/u, - ); - }, - ); - }); - it('rejects bridge and environment capability growth inside a transitive legacy AppShell helper', async () => { await withDesktopFixture( transitiveAppShellFiles(` diff --git a/apps/desktop/src/renderer/README.md b/apps/desktop/src/renderer/README.md index fb9b0cbaef..c9ea51eaca 100644 --- a/apps/desktop/src/renderer/README.md +++ b/apps/desktop/src/renderer/README.md @@ -109,10 +109,8 @@ that a capability has reached its final owner. The directory dependency rules remain executable. A later owner contract can add verifiable owner paths and public entries once each mixed legacy capability has been split precisely. -Exact Hook names remain visible in the generated ledger. A legitimate Hook -replacement must use a one-time `hookTransitions` entry: the new call count has -to be paid one-for-one by removal of the named old Hook in the same debt file, -the total Hook budget may not grow, and historical transitions cannot be reused. +Exact Hook names remain visible in the generated ledger, and no tracked Hook +call count may grow in a debt file. The separate AppShell render-scope inventory tracks which calls still execute above the whole renderer tree; this architecture checker governs the broader root and transitive capability debt.