import fs from 'node:fs';
import path from 'node:path';
import ts from 'typescript';
const repoRoot = path.resolve(import.meta.dirname, '..');
const sourceRoot = path.join(repoRoot, 'src', 'web-ui', 'src');
const componentRoot = path.join(sourceRoot, 'component-library', 'components');
const sceneRoot = path.join(sourceRoot, 'app', 'scenes');
const registryFile = path.join(sourceRoot, 'infrastructure', 'appearance', 'registry', 'defaultAppearanceRegistry.ts');
const retiredOwnershipFile = path.join(sourceRoot, 'infrastructure', 'appearance', 'registry', 'appearanceSourceOwnership.ts');
const crossBoundaryRoots = [
path.join(repoRoot, 'MiniApp'),
path.join(repoRoot, 'tests', 'e2e', 'specs'),
path.join(repoRoot, 'tests', 'e2e', 'helpers'),
path.join(repoRoot, 'src', 'crates', 'contracts', 'product-domains', 'src', 'miniapp'),
path.join(repoRoot, 'src', 'crates', 'services', 'services-integrations', 'src', 'canvas'),
path.join(repoRoot, 'src', 'crates', 'assembly', 'core', 'builtin_skills', 'bitfun-canvas'),
path.join(repoRoot, 'src', 'crates', 'assembly', 'core', 'builtin_skills', 'miniapp-dev'),
];
const failures = [];
const warnings = [];
const compoundOwnerMinimumParts = 4;
const forbiddenAggregateItemSurfaces = new Set(['workbench', 'flow-chat']);
const retiredAppearancePaths = [
'scripts/generate-startup-theme-bootstrap.mjs',
'scripts/generate-startup-theme-bootstrap.test.mjs',
'src/apps/desktop/src/theme.rs',
'src/apps/desktop/src/generated/startup_theme_bootstrap.json',
'src/web-ui/src/infrastructure/theme',
'src/web-ui/src/infrastructure/skin',
];
for (const retiredPath of retiredAppearancePaths) {
if (fs.existsSync(path.join(repoRoot, retiredPath))) {
failures.push(`${retiredPath}: retired Theme/Skin path is forbidden`);
}
}
const finalStateContractFiles = [
'package.json',
'scripts/generate-startup-appearance-bootstrap.mjs',
'src/apps/desktop/src/appearance.rs',
'src/web-ui/src/infrastructure/config/index.ts',
'src/web-ui/src/shared/context-menu-system/types/menu.types.ts',
'tests/e2e/helpers/performance-trace.ts',
'docs/development/ui-testids.md',
'docs/development/ui-testids-CN.md',
];
const retiredFinalStateContracts = [
['generate-startup-theme-bootstrap', /generate-startup-theme-bootstrap/],
['startup_theme_bootstrap', /startup_theme_bootstrap/],
['STARTUP_THEME_BOOTSTRAP', /STARTUP_THEME_BOOTSTRAP/],
['__BITFUN_BOOTSTRAP_THEME', /__BITFUN_BOOTSTRAP_THEME/],
['prepare_theme', /prepare_theme/],
['prepareThemeDurationMs', /prepareThemeDurationMs/],
['theme-switching', /theme-switching/],
['MenuTheme', /\bMenuTheme\b/],
['appearance-theme-option', /appearance-theme-option/],
['data-theme-id', /data-theme-id/],
['legacy bootstrap CSS token', /--(?:color-(?:bg|text|accent)|border|element)-/],
];
for (const contractFile of finalStateContractFiles) {
const absolute = path.join(repoRoot, contractFile);
if (!fs.existsSync(absolute)) continue;
const source = fs.readFileSync(absolute, 'utf8');
for (const [name, pattern] of retiredFinalStateContracts) {
if (pattern.test(source)) {
failures.push(`${contractFile}: retired final-state contract ${name} is forbidden`);
}
}
}
if (fs.existsSync(path.join(repoRoot, 'docs', 'architecture', 'skin-package-system.md'))) {
failures.push('docs/architecture/skin-package-system.md: retired Appearance document alias is forbidden');
}
if (fs.existsSync(path.join(repoRoot, 'tests', 'e2e', 'specs', 'l0-theme.spec.ts'))) {
failures.push('tests/e2e/specs/l0-theme.spec.ts: retired Web UI Theme E2E alias is forbidden');
}
if (fs.existsSync(retiredOwnershipFile)) {
failures.push(`${relative(retiredOwnershipFile)}: directory-level Appearance source ownership is forbidden`);
}
const ignoredWalkDirectories = new Set(['node_modules', 'dist', 'build']);
function walk(directory) {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
const absolute = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (ignoredWalkDirectories.has(entry.name)) return [];
return walk(absolute);
}
return /\.(?:css|scss|ts|tsx)$/.test(entry.name) ? [absolute] : [];
});
}
function walkContractSources(directory) {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
const absolute = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (ignoredWalkDirectories.has(entry.name)) return [];
return walkContractSources(absolute);
}
return /\.(?:css|d\.ts|html|js|json|md|rs|scss|ts|tsx)$/.test(entry.name) ? [absolute] : [];
});
}
function relative(file) {
return path.relative(repoRoot, file);
}
function extractObject(source, start) {
const open = source.indexOf('{', start);
if (open < 0) return null;
let depth = 0;
let quote = null;
let escaped = false;
for (let index = open; index < source.length; index += 1) {
const char = source[index];
if (quote) {
if (escaped) escaped = false;
else if (char === '\\') escaped = true;
else if (char === quote) quote = null;
continue;
}
if (char === '\'' || char === '"' || char === '`') {
quote = char;
continue;
}
if (char === '{') depth += 1;
if (char === '}') {
depth -= 1;
if (depth === 0) return source.slice(open, index + 1);
}
}
return null;
}
function extractArrayProperty(source, property) {
const match = new RegExp(`\\b${property}\\s*:`).exec(source);
if (!match) return null;
const open = source.indexOf('[', match.index + match[0].length);
if (open < 0) return null;
let depth = 0;
let quote = null;
let escaped = false;
for (let index = open; index < source.length; index += 1) {
const char = source[index];
if (quote) {
if (escaped) escaped = false;
else if (char === '\\') escaped = true;
else if (char === quote) quote = null;
continue;
}
if (char === '\'' || char === '"' || char === '`') {
quote = char;
continue;
}
if (char === '[') depth += 1;
if (char === ']') {
depth -= 1;
if (depth === 0) return source.slice(open, index + 1);
}
}
return null;
}
function extractTopLevelObjects(arraySource) {
const objects = [];
let index = 0;
while (index < arraySource.length) {
const open = arraySource.indexOf('{', index);
if (open < 0) break;
const object = extractObject(arraySource, open);
if (!object) break;
objects.push(object);
index = open + object.length;
}
return objects;
}
function extractStringProperty(source, property) {
const match = new RegExp(`\\b${property}\\s*:\\s*(?:'((?:\\\\.|[^'\\\\])*)'|"((?:\\\\.|[^"\\\\])*)")`).exec(source);
return match ? (match[1] ?? match[2]) : null;
}
function literalAttributes(source, attribute) {
return new Set([...source.matchAll(new RegExp(`${attribute}=["']([^"']+)["']`, 'g'))].map(match => match[1]));
}
function importedVisualClassIds(file, source) {
const classIds = new Set();
for (const match of source.matchAll(/import\s+['"]([^'"]+\.(?:css|scss))['"]/g)) {
const styleFile = path.resolve(path.dirname(file), match[1]);
if (!fs.existsSync(styleFile)) continue;
const styleSource = fs.readFileSync(styleFile, 'utf8');
for (const classMatch of styleSource.matchAll(/\.([A-Za-z_][\w-]*)/g)) {
const classId = classMatch[1];
if (!classId.startsWith('xterm') && !classId.startsWith('is-') && !classId.includes('--')) {
classIds.add(classId);
}
}
for (const nestedMatch of styleSource.matchAll(/&(__[A-Za-z_][\w-]*)/g)) {
classIds.add(nestedMatch[1]);
}
}
return classIds;
}
function intrinsicName(node) {
const tagName = node.tagName;
return ts.isIdentifier(tagName) && /^[a-z]/.test(tagName.text) ? tagName.text : null;
}
function jsxTagName(node) {
return ts.isIdentifier(node.tagName) ? node.tagName.text : null;
}
function jsxAttribute(node, name) {
const attribute = node.attributes.properties.find(property => (
ts.isJsxAttribute(property) && property.name.text === name
));
if (!attribute || !ts.isJsxAttribute(attribute)) return { present: false, value: null };
if (!attribute.initializer) return { present: true, value: null };
if (ts.isStringLiteral(attribute.initializer)) {
return { present: true, value: attribute.initializer.text };
}
if (ts.isJsxExpression(attribute.initializer)
&& attribute.initializer.expression
&& ts.isStringLiteral(attribute.initializer.expression)) {
return { present: true, value: attribute.initializer.expression.text };
}
return { present: true, value: null };
}
function jsxLiteralAttribute(node, name) {
return jsxAttribute(node, name).value;
}
function jsxAttributeStringLiterals(node, name) {
const attribute = node.attributes.properties.find(property => (
ts.isJsxAttribute(property) && property.name.text === name
));
if (!attribute || !ts.isJsxAttribute(attribute) || !attribute.initializer) return { present: false, dynamic: false, values: [] };
if (ts.isStringLiteral(attribute.initializer)) {
return { present: true, dynamic: false, values: [attribute.initializer.text] };
}
if (!ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression) {
return { present: true, dynamic: true, values: [] };
}
const values = new Set();
const collectReturnedValues = expression => {
if (ts.isParenthesizedExpression(expression)
|| ts.isAsExpression(expression)
|| ts.isTypeAssertionExpression(expression)
|| ts.isNonNullExpression(expression)
|| ts.isSatisfiesExpression(expression)) {
collectReturnedValues(expression.expression);
return;
}
if (ts.isStringLiteralLike(expression)) {
values.add(expression.text);
return;
}
if (ts.isConditionalExpression(expression)) {
collectReturnedValues(expression.whenTrue);
collectReturnedValues(expression.whenFalse);
return;
}
if (ts.isBinaryExpression(expression)) {
if (expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
collectReturnedValues(expression.right);
} else if (expression.operatorToken.kind === ts.SyntaxKind.BarBarToken
|| expression.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
collectReturnedValues(expression.left);
collectReturnedValues(expression.right);
}
return;
}
if (ts.isArrayLiteralExpression(expression)) {
expression.elements.forEach(collectReturnedValues);
return;
}
if (ts.isCallExpression(expression)
&& ts.isPropertyAccessExpression(expression.expression)
&& expression.expression.name.text === 'join') {
let receiver = expression.expression.expression;
while (ts.isCallExpression(receiver) && ts.isPropertyAccessExpression(receiver.expression)) {
receiver = receiver.expression.expression;
}
if (ts.isArrayLiteralExpression(receiver)) {
receiver.elements.forEach(collectReturnedValues);
}
}
};
collectReturnedValues(attribute.initializer.expression);
return { present: true, dynamic: true, values: [...values] };
}
function datasetPropertyForAttribute(attribute) {
return attribute.replace(/^data-/, '').replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}
function analyzeStyledOwnerContract(file, source, visualClassIds) {
const ast = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
let hasIntrinsicContract = false;
let hasBaseToolCardContract = false;
let hasConfigPageLayoutContract = false;
let styledIntrinsicNodeCount = 0;
const declaredPartIds = new Set();
const usedVisualClassIds = new Set();
const collectVisualClassIds = text => {
for (const match of text.matchAll(/[A-Za-z_][\w-]*/g)) {
if (visualClassIds.has(match[0])) usedVisualClassIds.add(match[0]);
}
};
const visit = node => {
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
const surface = jsxLiteralAttribute(node, 'data-bf-component')
?? jsxLiteralAttribute(node, 'data-bf-scene');
const part = jsxLiteralAttribute(node, 'data-bf-part');
if (part) declaredPartIds.add(part);
if (intrinsicName(node)) {
if (jsxAttribute(node, 'className').present) styledIntrinsicNodeCount += 1;
if (surface && part) {
hasIntrinsicContract = true;
}
}
const tagName = jsxTagName(node);
if (tagName === 'BaseToolCard') {
hasBaseToolCardContract = true;
}
if (tagName === 'ConfigPageLayout' && surface && part) {
hasConfigPageLayoutContract = true;
}
}
if (ts.isStringLiteralLike(node)) collectVisualClassIds(node.text);
if (ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) {
collectVisualClassIds(node.text);
}
ts.forEachChild(node, visit);
};
visit(ast);
return {
hasIntrinsicContract,
hasBaseToolCardContract,
hasConfigPageLayoutContract,
styledIntrinsicNodeCount,
declaredPartIds,
usedVisualClassIds,
};
}
const sourceFiles = walk(sourceRoot);
const sources = new Map(sourceFiles.map(file => [file, fs.readFileSync(file, 'utf8')]));
const productionSources = [...sources.entries()].filter(([file]) => !/\.(?:test|spec)\.[jt]sx?$/.test(file));
const productionCodeSources = productionSources.filter(([file]) => /\.(?:ts|tsx)$/.test(file));
const aggregate = productionSources.map(([, source]) => source).join('\n');
const registeredSource = fs.readFileSync(registryFile, 'utf8');
const registeredComponentExports = new Set(
[...registeredSource.matchAll(/\.registerComponent\((\w+)\)/g)].map(match => match[1]),
);
const registeredSceneExports = new Set(
[...registeredSource.matchAll(/\.registerScene\((\w+)\)/g)].map(match => match[1]),
);
const rendererTypesSource = fs.readFileSync(path.join(sourceRoot, 'infrastructure', 'appearance', 'types', 'index.ts'), 'utf8');
if (/settings:\s*Record<string,\s*unknown>/.test(rendererTypesSource)) {
failures.push('Appearance renderer settings must use the closed host-owned settings map');
}
if (!rendererTypesSource.includes('export interface AppearanceRendererSettingsMap')) {
failures.push('Appearance renderer settings must declare AppearanceRendererSettingsMap');
}
const descriptors = [];
for (const [file, source] of productionCodeSources) {
const declaration = /export const\s+(\w+)\s*:\s*AppearanceSurfaceDescriptor\s*=\s*/g;
for (const match of source.matchAll(declaration)) {
const body = extractObject(source, match.index + match[0].length);
const id = body?.match(/\bid:\s*['"]([^'"]+)['"]/)?.[1];
const partsBody = body ? extractArrayProperty(body, 'parts') : null;
const statesBody = body ? extractArrayProperty(body, 'states') : null;
const facetsBody = body ? extractArrayProperty(body, 'facets') : null;
if (!body || !id || !partsBody) {
failures.push(`${relative(file)}: ${match[1]} must declare literal id and parts`);
continue;
}
const parts = [...partsBody.matchAll(/\bid:\s*['"]([^'"]+)['"]/g)].map(part => part[1]);
const states = statesBody ? extractTopLevelObjects(statesBody).map(state => ({
id: state.match(/\bid:\s*['"]([^'"]+)['"]/)?.[1],
kind: state.match(/\bkind:\s*['"]([^'"]+)['"]/)?.[1],
part: state.match(/\bpart:\s*['"]([^'"]+)['"]/)?.[1],
suffix: extractStringProperty(state, 'suffix'),
})).filter(state => state.id) : [];
const facets = facetsBody ? extractTopLevelObjects(facetsBody).map(facet => ({
id: facet.match(/\bid:\s*['"]([^'"]+)['"]/)?.[1],
attribute: facet.match(/\battribute:\s*['"]([^'"]+)['"]/)?.[1],
values: [...(extractArrayProperty(facet, 'values') ?? '').matchAll(/['"]([^'"]+)['"]/g)].map(value => value[1]),
})).filter(facet => facet.id && facet.attribute) : [];
const registeredAsComponent = registeredComponentExports.has(match[1]);
const registeredAsScene = registeredSceneExports.has(match[1]);
if (registeredAsComponent === registeredAsScene) {
failures.push(`${relative(file)}: ${match[1]} must be registered exactly once as a component or scene`);
}
descriptors.push({
file,
exportName: match[1],
id,
parts,
states,
facets,
kind: registeredAsScene ? 'scene' : 'component',
});
}
}
const duplicateIds = descriptors.filter((item, index) => descriptors.findIndex(other => other.id === item.id) !== index);
duplicateIds.forEach(item => failures.push(`${relative(item.file)}: duplicate appearance surface id ${item.id}`));
descriptors
.filter(item => /(?:^|[-_.])(?:v\d+|legacy)(?:$|[-_.])/.test(item.id))
.forEach(item => failures.push(`${relative(item.file)}: transitional Appearance surface id ${item.id} is forbidden`));
const descriptorsById = new Map(descriptors.map(descriptor => [descriptor.id, descriptor]));
const domSurfaceParts = new Map(descriptors.map(descriptor => [descriptor.id, new Set()]));
const domSurfaceStates = new Map(descriptors.map(descriptor => [descriptor.id, new Set()]));
const domSurfaceDynamicStates = new Set();
const domSurfaceFacets = new Map(descriptors.map(descriptor => [descriptor.id, new Map()]));
let domContractCount = 0;
for (const [file, source] of productionCodeSources.filter(([candidate]) => candidate.endsWith('.tsx'))) {
const ast = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
const visit = node => {
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
const component = jsxAttribute(node, 'data-bf-component');
const scene = jsxAttribute(node, 'data-bf-scene');
const part = jsxAttribute(node, 'data-bf-part');
const state = jsxAttribute(node, 'data-bf-state');
const location = ast.getLineAndCharacterOfPosition(node.getStart(ast));
const sourceLocation = `${relative(file)}:${location.line + 1}`;
const surfaceAttributeCount = Number(component.present) + Number(scene.present);
if (surfaceAttributeCount > 1) {
failures.push(`${sourceLocation}: DOM node cannot declare both data-bf-component and data-bf-scene`);
}
if (surfaceAttributeCount !== Number(part.present)) {
failures.push(`${sourceLocation}: Appearance surface and part attributes must be declared together on the same DOM node`);
}
if (component.present && component.value === null) {
failures.push(`${sourceLocation}: data-bf-component must use a string literal`);
}
if (scene.present && scene.value === null) {
failures.push(`${sourceLocation}: data-bf-scene must use a string literal`);
}
if (part.present && part.value === null) {
failures.push(`${sourceLocation}: data-bf-part must use a string literal`);
}
const surfaceId = component.value ?? scene.value;
if (surfaceId && part.value) {
if (forbiddenAggregateItemSurfaces.has(surfaceId) && part.value === 'item') {
failures.push(`${sourceLocation}: aggregate Appearance contract ${surfaceId}.item is forbidden; the visible owner must declare a dedicated surface`);
}
domContractCount += 1;
const descriptor = descriptorsById.get(surfaceId);
const domKind = component.value ? 'component' : 'scene';
if (!descriptor) {
failures.push(`${sourceLocation}: unknown Appearance ${domKind} id ${surfaceId}`);
} else {
if (descriptor.kind !== domKind) {
failures.push(`${sourceLocation}: ${surfaceId} is registered as a ${descriptor.kind}, not a ${domKind}`);
}
if (!descriptor.parts.includes(part.value)) {
failures.push(`${sourceLocation}: unknown Appearance part ${surfaceId}.${part.value}`);
} else {
domSurfaceParts.get(surfaceId)?.add(part.value);
}
const stateAttribute = jsxAttributeStringLiterals(node, 'data-bf-state');
if (stateAttribute.present && stateAttribute.dynamic) {
domSurfaceDynamicStates.add(surfaceId);
}
const stateValues = stateAttribute.values
.flatMap(value => value.split(/\s+/).filter(Boolean));
stateValues.forEach(stateId => {
domSurfaceStates.get(surfaceId)?.add(stateId);
const knownStateTokens = descriptor.states.map(candidate => (
candidate.suffix?.match(/\[data-bf-state~=["']([^"']+)["']\]/)?.[1] ?? candidate.id
));
if (!knownStateTokens.includes(stateId)) {
failures.push(`${sourceLocation}: unknown Appearance state ${surfaceId}.${stateId}`);
}
});
for (const facet of descriptor.facets) {
const facetAttribute = jsxAttributeStringLiterals(node, facet.attribute);
if (!facetAttribute.present) continue;
const facetEntry = domSurfaceFacets.get(surfaceId)?.get(facet.attribute) ?? { dynamic: false, values: new Set() };
facetEntry.dynamic ||= facetAttribute.dynamic;
facetAttribute.values.forEach(value => facetEntry.values.add(value));
domSurfaceFacets.get(surfaceId)?.set(facet.attribute, facetEntry);
facetAttribute.values.forEach(value => {
if (!facet.values.includes(value)) {
failures.push(`${sourceLocation}: unknown Appearance facet value ${surfaceId}.${facet.id}.${value}`);
}
});
}
}
}
}
ts.forEachChild(node, visit);
};
visit(ast);
const dynamicContractPattern = /\b([A-Za-z_$][\w$]*)\.dataset\.bfComponent\s*=\s*['"]([^'"]+)['"]\s*;[\s\S]{0,240}?\b\1\.dataset\.bfPart\s*=\s*['"]([^'"]+)['"]\s*;/g;
for (const match of source.matchAll(dynamicContractPattern)) {
const surfaceId = match[2];
const partId = match[3];
const line = source.slice(0, match.index).split(/\r?\n/).length;
const sourceLocation = `${relative(file)}:${line}`;
const descriptor = descriptorsById.get(surfaceId);
domContractCount += 1;
if (!descriptor) {
failures.push(`${sourceLocation}: unknown dynamic Appearance component id ${surfaceId}`);
continue;
}
if (descriptor.kind !== 'component') {
failures.push(`${sourceLocation}: dynamic DOM contract ${surfaceId} must reference a component descriptor`);
continue;
}
if (!descriptor.parts.includes(partId)) {
failures.push(`${sourceLocation}: unknown dynamic Appearance part ${surfaceId}.${partId}`);
continue;
}
domSurfaceParts.get(surfaceId)?.add(partId);
const dynamicNodeSource = source.slice(match.index, match.index + 800);
for (const facet of descriptor.facets) {
const datasetProperty = datasetPropertyForAttribute(facet.attribute);
const assignmentPattern = new RegExp(`\\b${match[1]}\\.dataset\\.${datasetProperty}\\s*=`);
if (!assignmentPattern.test(dynamicNodeSource)) continue;
domSurfaceFacets.get(surfaceId)?.set(facet.attribute, { dynamic: true, values: new Set() });
}
}
}
for (const descriptor of descriptors) {
const kind = descriptor.kind === 'scene' ? 'Scene' : 'Component';
for (const part of descriptor.parts) {
if (!domSurfaceParts.get(descriptor.id)?.has(part)) {
failures.push(`${relative(descriptor.file)}: registered part ${descriptor.id}.${part} has no exact DOM contract`);
}
}
for (const state of descriptor.states) {
const stateToken = state.suffix?.match(/\[data-bf-state~=["']([^"']+)["']\]/)?.[1];
if (state.kind === 'ancestorPart' && state.part && !descriptor.parts.includes(state.part)) {
failures.push(`${relative(descriptor.file)}: state ${descriptor.id}.${state.id} references unknown ancestor part ${state.part}`);
}
if (stateToken
&& !domSurfaceStates.get(descriptor.id)?.has(stateToken)
&& !domSurfaceDynamicStates.has(descriptor.id)) {
failures.push(`${relative(descriptor.file)}: registered state ${descriptor.id}.${state.id} has no DOM state source (${stateToken})`);
}
}
for (const facet of descriptor.facets) {
if (!domSurfaceFacets.get(descriptor.id)?.has(facet.attribute)) {
failures.push(`${relative(descriptor.file)}: registered facet ${descriptor.id}.${facet.id} has no DOM attribute source (${facet.attribute})`);
}
}
if (!registeredSource.includes(`register${kind}(${descriptor.exportName})`)) {
failures.push(`${relative(descriptor.file)}: ${descriptor.exportName} is not registered in defaultAppearanceRegistry`);
}
}
for (const directory of fs.readdirSync(componentRoot, { withFileTypes: true }).filter(entry => entry.isDirectory())) {
const absolute = path.join(componentRoot, directory.name);
const productionTsx = walk(absolute).filter(file => file.endsWith('.tsx') && !/\.(?:test|spec)\.tsx$/.test(file));
if (productionTsx.length > 0 && !fs.existsSync(path.join(absolute, 'appearance.ts'))) {
failures.push(`${relative(absolute)}: production component directory must own appearance.ts`);
}
}
for (const sceneFile of walk(sceneRoot).filter(file => file.endsWith('Scene.tsx') && !/\.(?:test|spec)\.tsx$/.test(file))) {
const appearanceFile = path.join(path.dirname(sceneFile), 'appearance.ts');
if (!fs.existsSync(appearanceFile)) {
failures.push(`${relative(sceneFile)}: top-level Scene must own colocated appearance.ts`);
continue;
}
const sceneIds = literalAttributes(sources.get(sceneFile) ?? '', 'data-bf-scene');
const colocatedDescriptorIds = new Set(descriptors.filter(item => item.file === appearanceFile).map(item => item.id));
if (sceneIds.size === 0) {
failures.push(`${relative(sceneFile)}: top-level Scene must expose a literal data-bf-scene root contract`);
} else if (![...sceneIds].some(id => colocatedDescriptorIds.has(id))) {
failures.push(`${relative(sceneFile)}: no data-bf-scene value matches a descriptor in colocated appearance.ts`);
}
}
const visualEntryPoints = new Set([
'main.tsx',
'component-library/preview/main.tsx',
'tools/bitfun-canvas/runtime/entry.tsx',
]);
const styledProductionTsx = productionCodeSources.filter(([file, source]) => {
if (!file.endsWith('.tsx')) return false;
const sourceRelative = path.relative(sourceRoot, file).replaceAll(path.sep, '/');
if (visualEntryPoints.has(sourceRelative)) return false;
return /\.(?:css|scss)['"]/.test(source);
});
const allVisualClassIds = new Set();
for (const [file, source] of productionSources) {
if (!/\.(?:css|scss)$/.test(file)) continue;
for (const match of source.matchAll(/\.([A-Za-z_][\w-]*)/g)) {
if (!match[1].startsWith('xterm') && !match[1].startsWith('is-') && !match[1].includes('--')) {
allVisualClassIds.add(match[1]);
}
}
for (const match of source.matchAll(/&(__[A-Za-z_][\w-]*)/g)) allVisualClassIds.add(match[1]);
}
const baseToolCardSource = sources.get(path.join(sourceRoot, 'flow_chat', 'tool-cards', 'BaseToolCard.tsx')) ?? '';
if (!baseToolCardSource.includes('data-bf-component="tool-card"')
|| !baseToolCardSource.includes('data-bf-part="root"')
|| !baseToolCardSource.includes('data-bf-part="surface"')
|| !baseToolCardSource.includes('data-bf-part="header"')
|| !baseToolCardSource.includes('data-bf-part="expanded"')
|| !baseToolCardSource.includes('data-bf-part="error"')) {
failures.push('BaseToolCard must project the shared multi-part tool-card Appearance contract');
}
const configPageLayoutSource = sources.get(path.join(sourceRoot, 'infrastructure', 'config', 'components', 'common', 'ConfigPageLayout.tsx')) ?? '';
if (!configPageLayoutSource.includes('...props')
|| !configPageLayoutSource.includes('data-bf-component="config"')
|| !configPageLayoutSource.includes('data-bf-part="root"')) {
failures.push('ConfigPageLayout must forward caller Appearance attributes to its real config root');
}
for (const [file, source] of styledProductionTsx) {
const visualClassIds = importedVisualClassIds(file, source);
const contract = analyzeStyledOwnerContract(file, source, visualClassIds);
if (!contract.hasIntrinsicContract
&& !contract.hasBaseToolCardContract
&& !contract.hasConfigPageLayoutContract) {
failures.push(`${relative(file)}: styled production component must expose a direct DOM Appearance contract or an approved host-forwarded contract`);
continue;
}
const visualClassCount = contract.usedVisualClassIds.size;
if (visualClassCount >= compoundOwnerMinimumParts
&& contract.styledIntrinsicNodeCount >= compoundOwnerMinimumParts) {
const requiredPartCount = Math.min(
compoundOwnerMinimumParts,
visualClassCount,
contract.styledIntrinsicNodeCount,
);
if (contract.declaredPartIds.size < requiredPartCount) {
failures.push(
`${relative(file)}: compound styled owner (${visualClassCount} used visual classes, ${contract.styledIntrinsicNodeCount} styled DOM nodes) must expose at least ${requiredPartCount} distinct Appearance parts`,
);
}
}
}
for (const [file, source] of productionCodeSources.filter(([candidate]) => candidate.endsWith('.tsx'))) {
if (styledProductionTsx.some(([styledFile]) => styledFile === file)) continue;
const contract = analyzeStyledOwnerContract(file, source, allVisualClassIds);
if (contract.usedVisualClassIds.size < 8 || contract.hasIntrinsicContract
|| contract.hasBaseToolCardContract || contract.hasConfigPageLayoutContract) continue;
warnings.push(`${relative(file)}: uses ${contract.usedVisualClassIds.size} shared visual classes without a direct Appearance contract; review whether it needs a dedicated surface`);
}
const adapterFiles = productionCodeSources.filter(([file]) => file.includes(`${path.sep}infrastructure${path.sep}appearance${path.sep}adapters${path.sep}`) && file.endsWith('AppearanceAdapter.ts'));
for (const [file, source] of adapterFiles) {
const exportName = source.match(/export const\s+(\w+)/)?.[1];
const id = source.match(/(?:readonly\s+)?id\s*(?:=|:)\s*['"]([^'"]+)['"]/)?.[1];
if (!exportName || !id) {
failures.push(`${relative(file)}: renderer adapter must export a singleton with a literal id`);
continue;
}
if (!registeredSource.includes(`registerRenderer(${exportName})`)) {
failures.push(`${relative(file)}: ${exportName} is not registered in defaultAppearanceRegistry`);
}
if (!aggregate.includes(`'${id}':`) && !aggregate.includes(`${id}:`)) {
failures.push(`${relative(file)}: renderer ${id} has no Appearance package settings producer`);
}
if (/extends\s+Record<string,\s*unknown>|Readonly<Record<string,\s*unknown>>/.test(source)) {
failures.push(`${relative(file)}: renderer adapters must not expose open Record<string, unknown> payloads`);
}
}
const cssTokenAdapterSource = fs.readFileSync(path.join(sourceRoot, 'infrastructure', 'appearance', 'adapters', 'CssTokenAppearanceAdapter.ts'), 'utf8');
if (!cssTokenAdapterSource.includes('APPEARANCE_CSS_TOKEN_NAMES') || cssTokenAdapterSource.includes("startsWith(ALLOWED_TOKEN_PREFIX)")) {
failures.push('CssTokenAppearanceAdapter must validate against the closed host token registry');
}
const widgetAdapterSource = fs.readFileSync(path.join(sourceRoot, 'infrastructure', 'appearance', 'adapters', 'WidgetAppearanceAdapter.ts'), 'utf8');
if (!widgetAdapterSource.includes('WIDGET_APPEARANCE_VARIABLE_NAMES')) {
failures.push('WidgetAppearanceAdapter must validate against the closed widget variable registry');
}
const runtimeSource = fs.readFileSync(path.join(sourceRoot, 'infrastructure', 'appearance', 'runtime', 'AppearanceRuntime.ts'), 'utf8');
if (!runtimeSource.includes('getRendererAdapters()')) {
failures.push('AppearanceRuntime must consume every registered renderer adapter');
}
const compilerSource = fs.readFileSync(path.join(sourceRoot, 'infrastructure', 'appearance', 'compiler', 'AppearanceCompiler.ts'), 'utf8');
if (/\bfetch\s*\(/.test(compilerSource) || /127\.0\.0\.1:7469|#region agent log/.test(compilerSource)) {
failures.push('AppearanceCompiler must remain a pure compiler without debug network side effects');
}
for (const [file, source] of productionSources) {
if (source.includes('data-bf-kind')) {
failures.push(`${relative(file)}: data-bf-kind is forbidden; use a dedicated surface, semantic part, facet, or state`);
}
const normalizedFile = relative(file).replaceAll('\\', '/');
if (/appearance(?:[-_.]?(?:v\d+|new|legacy))/i.test(normalizedFile)
|| /\b(?:AppearanceV\d+|appearanceV\d+|newAppearance|appearanceNew|legacyAppearance|appearanceLegacy)\b/.test(source)) {
failures.push(`${relative(file)}: versioned or transitional Appearance naming is forbidden`);
}
if (/\.(?:css|scss)$/.test(file) && /prefers-color-scheme/.test(source)) {
failures.push(`${relative(file)}: production styles must follow data-bf-appearance-mode instead of OS color-scheme media queries`);
}
if (source.includes('createPortal(') && !source.includes('getAppearanceOverlayHost')) {
failures.push(`${relative(file)}: createPortal must target getAppearanceOverlayHost()`);
}
if (/infrastructure[\\/]skin|\bSkinService\b|\buseSkin\b/.test(source)) {
failures.push(`${relative(file)}: legacy Skin runtime reference is forbidden`);
}
if (/\bThemeService\b|\bthemeService\b|\buseTheme\b|\buseThemeStore\b|ThemeAppearanceBridge/.test(source)) {
failures.push(`${relative(file)}: legacy Theme runtime reference is forbidden`);
}
if (/data-theme(?:-type)?|data-bf-theme|bitfun\/request-theme|themeChange|onThemeChange/.test(source)) {
failures.push(`${relative(file)}: legacy Theme DOM or bridge contract is forbidden`);
}
if (/--(?:color|border|element|git-color|scrollbar|shadow|blur|size|opacity|motion|easing|font|line-height|btn|flowchat|scene)-/.test(source)) {
failures.push(`${relative(file)}: legacy CSS token prefix is forbidden`);
}
if (/--(?:glass|tool-card)-/.test(source)) {
failures.push(`${relative(file)}: surface-local visual token prefix is forbidden`);
}
if (/theme-config|theme-card|theme-preview|preview-theme-selector|deep-skin/.test(source)) {
failures.push(`${relative(file)}: retired Theme/Skin presentation naming is forbidden`);
}
if (/section\s*===\s*['"]theme['"]/.test(source)) {
failures.push(`${relative(file)}: retired Theme settings deep-link alias is forbidden`);
}
if (file.includes(`${path.sep}infrastructure${path.sep}appearance${path.sep}`)
&& source.includes('selectorSuffix')) {
failures.push(`${relative(file)}: retired selectorSuffix state contract is forbidden`);
}
}
const crossBoundaryForbiddenContracts = [
['hostTheme', /\bhostTheme\b/],
['CanvasHostTheme', /\bCanvasHostTheme\b/],
['bitfun-canvas-theme', /bitfun-canvas-theme/],
['onThemeChange', /\bonThemeChange\b/],
['data-theme-type', /data-theme-type/],
['bitfun/request-theme', /bitfun\/request-theme/],
['ThemeService', /\bThemeService\b/],
['SkinService', /\bSkinService\b/],
['tools/editor/themes', /tools[\\/]editor[\\/]themes/],
['editor.theme', /\beditor\.theme\b/],
['terminal.theme', /\bterminal\.theme\b/],
['app.theme', /\bapp\.theme\b/],
['theme-config', /theme-config/],
['--glass-*', /--glass-[a-z0-9-]+/i],
['--tool-card-*', /--tool-card-[a-z0-9-]+/i],
];
const crossBoundaryFiles = crossBoundaryRoots.flatMap(walkContractSources);
for (const file of crossBoundaryFiles) {
const source = fs.readFileSync(file, 'utf8');
for (const [name, pattern] of crossBoundaryForbiddenContracts) {
if (pattern.test(source)) failures.push(`${relative(file)}: retired cross-boundary contract ${name} is forbidden`);
}
}
if (failures.length > 0) {
console.error('Appearance contract audit failed:');
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
if (warnings.length > 0) {
console.warn('Appearance contract audit warnings:');
warnings.forEach(warning => console.warn(`- ${warning}`));
}
console.log(`Appearance contract audit passed (${descriptors.length} surfaces, ${domContractCount} DOM contracts, ${styledProductionTsx.length} styled component owners, ${warnings.length} shared-style owner warnings, ${sourceFiles.length} Web UI files, ${crossBoundaryFiles.length} cross-boundary files).`);