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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
101 changes: 0 additions & 101 deletions apps/desktop/scripts/check-renderer-architecture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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) ||
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 === '+') {
Expand Down Expand Up @@ -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)])),
Expand All @@ -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'
Expand Down Expand Up @@ -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}`);
Expand All @@ -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)) {
Expand Down
100 changes: 0 additions & 100 deletions apps/desktop/scripts/check-renderer-architecture.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ function architectureConfig({
legacyFiles = {},
legacyGrowthDirectories = [],
legacyPlatformImports = [],
hookTransitions = [],
rootDebt = {},
rootDebtClosure = {},
legacyRendererFiles = Object.keys(rootDebt),
Expand All @@ -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 ?? {},
Expand Down Expand Up @@ -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(`
Expand Down
6 changes: 2 additions & 4 deletions apps/desktop/src/renderer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down