chore: establish YCode replication baseline and TODO

This commit is contained in:
xiaohan.yuan committed 2026-10-17 02:43:49 +08:00
commit 6dc59f89ba
1118 files changed
+196002

No files matched your search

+307
View File
@@ -0,0 +1,307 @@
import { createHash } from 'node:crypto'
import { readFile, writeFile } from 'node:fs/promises'
import { dirname, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import postcss from 'postcss'
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
const exportDir = resolve(rootDir, '.tmp/rea-css-recovered')
const outputPath = resolve(rootDir, 'src/renderer/src/workbuddy/composer/composer-source.css')
const evidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'
const sourceSpecs = [
{
file: 'src-CyXHA0IC.css',
selectors: [
'.cr-input-box',
'.cr-input-box__main',
'.cr-input-container',
'.cr-input-editor-host',
'.cr-input-toolbar',
'.cr-input-toolbar__left',
'.cr-input-toolbar__right',
'.cr-input-toolbar__send',
'.cr-input-footer-item',
'.cr-input-footer-item:hover:not(:disabled)',
'.cr-input-footer-item.cr-input-footer-item--open:not(:disabled)',
'.cr-input-footer-item.cr-input-footer-item--open:not(:disabled) .cr-input-footer-item__label',
'.cr-input-footer-item.cr-input-footer-item--open:not(:disabled) .cr-input-footer-item__chevron',
'.cr-input-footer-item:focus-visible',
'.cr-input-footer-item__icon',
'.cr-input-footer-item__label',
'.cr-input-footer-item__label:after',
'.cr-permission-setting--danger.cr-input-footer-item',
'.cr-permission-setting--danger.cr-input-footer-item .cr-input-footer-item__icon',
'.cr-permission-setting--danger.cr-input-footer-item .cr-input-footer-item__label',
'.cr-permission-setting--danger.cr-input-footer-item .cr-input-footer-item__chevron',
'.cr-permission-setting--danger:hover.cr-input-footer-item',
'.cr-permission-setting--danger.cr-input-footer-item--open.cr-input-footer-item',
'.cr-permission-setting--danger:hover.cr-input-footer-item .cr-input-footer-item__icon',
'.cr-permission-setting--danger:hover.cr-input-footer-item .cr-input-footer-item__label',
'.cr-permission-setting--danger:hover.cr-input-footer-item .cr-input-footer-item__chevron',
'.cr-permission-setting--danger.cr-input-footer-item--open.cr-input-footer-item .cr-input-footer-item__icon',
'.cr-permission-setting--danger.cr-input-footer-item--open.cr-input-footer-item .cr-input-footer-item__label',
'.cr-permission-setting--danger.cr-input-footer-item--open.cr-input-footer-item .cr-input-footer-item__chevron',
'.cr-input-toolbar__left .cr-permission-setting.cr-input-footer-item',
'.cr-input-toolbar__left .cr-permission-setting.cr-input-footer-item:hover:not(:disabled)',
'.cr-input-toolbar__left .cr-permission-setting.cr-input-footer-item.cr-input-footer-item--open',
'.cr-send-button',
'.cr-send-button:hover:not(:disabled)',
'.cr-send-button:active:not(:disabled)',
'.cr-send-button:disabled',
'.cr-send-button--sending',
'.cr-send-button--stop',
'.cr-send-button--custom',
'.cr-send-button--custom:hover:not(:disabled)',
'.cr-send-button__icon',
'.cr-send-button__icon>svg',
'.cr-voice-trigger',
'.cr-voice-trigger:hover:not(:disabled)',
'.cr-voice-trigger--recording',
'.cr-voice-trigger--recording:hover:not(:disabled)',
'.cr-voice-trigger--loading',
'.cr-voice-trigger>svg'
]
},
{
file: 'contexts-BkjwYSZT.css',
selectors: [
'.conversation-input-area',
'.conversation-input-area__custom-floating',
'.conversation-input-area__input-stack',
'.conversation-input .cr-input-box',
'.conversation-input__box .cr-input-box__main',
'.conversation-input__box .cr-input-container',
'.conversation-input__box .cr-permission-setting:not(.cr-permission-setting--danger):not(:disabled)',
'.conversation-input__box .cr-permission-setting:not(.cr-permission-setting--danger):not(:disabled) .cr-input-footer-item__label'
]
}
]
function normalizeSelector(selector) {
return selector.trim().replace(/\s*([>+~])\s*/g, '$1').replace(/\s+/g, ' ')
}
function sourceHash(bytes) {
return createHash('sha256').update(bytes).digest('hex')
}
function formatRule(selector, declarations, source, sha256, note) {
const comment = `/* REA ${source.file}:1:${source.column}; sha256 ${sha256}${note ? `; ${note}` : ''} */`
const body = declarations.map((declaration) => ` ${declaration}`).join('\n')
return `${comment}\n${selector} {\n${body}\n}`
}
function declarationText(rule) {
return rule.nodes
.filter((node) => node.type === 'decl')
.map((node) => `${node.prop}: ${node.value}${node.important ? ' !important' : ''};`)
}
function collectRules(root, source, selectors) {
const allowed = new Set(selectors.map(normalizeSelector))
const records = []
root.walkRules((rule) => {
const selected = postcss.list.comma(rule.selector)
.map(normalizeSelector)
.filter((selector) => allowed.has(selector))
if (selected.length === 0) return
const declarations = declarationText(rule)
if (declarations.length === 0) return
records.push({
source,
column: rule.source?.start?.column ?? 1,
selectors: selected,
declarations
})
})
return records
}
function findRule(root, selector) {
const expected = normalizeSelector(selector)
let match
root.walkRules((rule) => {
if (match) return
if (postcss.list.comma(rule.selector).some((item) => normalizeSelector(item) === expected)) match = rule
})
if (!match) throw new Error(`REA stylesheet is missing required selector: ${selector}`)
return match
}
function declaration(rule, property) {
const match = rule.nodes.find((node) => node.type === 'decl' && node.prop === property)
if (!match) throw new Error(`REA rule ${rule.selector} is missing ${property}`)
return match.value
}
function resolveToken(token, tokens, seen = new Set()) {
if (seen.has(token)) throw new Error(`Cyclic REA token reference: ${token}`)
const value = tokens.get(token)
if (!value) throw new Error(`REA token is missing: ${token}`)
const simpleVar = /^var\((--[\w-]+)\)$/.exec(value.trim())
if (!simpleVar) return value.trim()
seen.add(token)
return resolveToken(simpleVar[1], tokens, seen)
}
const manifestBytes = await readFile(resolve(exportDir, 'manifest.json'))
const manifest = JSON.parse(manifestBytes.toString('utf8'))
if (manifest.sourceEvidenceId !== evidenceId) {
throw new Error(`REA CSS evidence changed: expected ${evidenceId}, found ${manifest.sourceEvidenceId ?? 'none'}`)
}
const sources = new Map()
for (const spec of sourceSpecs) {
const bytes = await readFile(resolve(exportDir, spec.file))
const record = manifest.files?.find((file) => file.file === spec.file)
if (!record?.sha256) throw new Error(`REA manifest has no SHA-256 for ${spec.file}`)
const actualHash = sourceHash(bytes)
if (actualHash !== record.sha256) {
throw new Error(`REA CSS digest mismatch for ${spec.file}: manifest ${record.sha256}, actual ${actualHash}`)
}
sources.set(spec.file, {
...spec,
bytes,
sha256: actualHash,
root: postcss.parse(bytes.toString('utf8'), { from: spec.file })
})
}
const extraFiles = ['src-Bh3YZ35H.css', 'tokens-Df-i-zsO.css']
for (const file of extraFiles) {
const bytes = await readFile(resolve(exportDir, file))
const record = manifest.files?.find((item) => item.file === file)
if (!record?.sha256) throw new Error(`REA manifest has no SHA-256 for ${file}`)
const actualHash = sourceHash(bytes)
if (actualHash !== record.sha256) {
throw new Error(`REA CSS digest mismatch for ${file}: manifest ${record.sha256}, actual ${actualHash}`)
}
sources.set(file, {
file,
bytes,
sha256: actualHash,
root: postcss.parse(bytes.toString('utf8'), { from: file })
})
}
const cy = sources.get('src-CyXHA0IC.css')
const contexts = sources.get('contexts-BkjwYSZT.css')
const button = sources.get('src-Bh3YZ35H.css')
const tokensRoot = sources.get('tokens-Df-i-zsO.css').root
const tokens = new Map()
tokensRoot.walkDecls((decl) => {
if (decl.prop.startsWith('--wb-') && !tokens.has(decl.prop)) tokens.set(decl.prop, decl.value)
})
const sourceRules = sourceSpecs.flatMap((spec) => {
const source = sources.get(spec.file)
return collectRules(source.root, source, spec.selectors)
})
for (const spec of sourceSpecs) {
const found = new Set(sourceRules
.filter((record) => record.source.file === spec.file)
.flatMap((record) => record.selectors))
const missing = spec.selectors.map(normalizeSelector).filter((selector) => !found.has(selector))
if (missing.length) throw new Error(`REA stylesheet ${spec.file} is missing selected rules: ${missing.join(', ')}`)
}
const slateEditorRule = findRule(cy.root, '.cr-input-editor-host [data-slate-editor]')
const focusedContainerRule = findRule(cy.root, '.cr-input-container--focused')
const sendButtonRule = findRule(cy.root, '.cr-send-button')
const sendIconRule = findRule(cy.root, '.cr-send-button__icon')
const nativeButtonRule = findRule(button.root, '.wb-button')
const iconOnlyRule = findRule(button.root, '.wb-button--icon-only')
const iconOnlyMediumRule = findRule(button.root, '.wb-button--icon-only.wb-button--medium')
const plainButtonRule = findRule(button.root, '.wb-button--plain')
const circleButtonRule = findRule(button.root, '.wb-button--circle')
const footerItemRule = findRule(cy.root, '.cr-input-footer-item')
const sendSize = resolveToken('--wb-button-height-medium', tokens)
const sendRadius = declaration(circleButtonRule, 'border-radius')
const iconOnlyWidth = declaration(iconOnlyMediumRule, 'width')
const iconOnlyHeight = declaration(iconOnlyMediumRule, 'height')
const footerItemHeight = declaration(footerItemRule, 'height')
if (sendSize !== '32px' || footerItemHeight !== sendSize || iconOnlyWidth !== 'var(--wb-button-height-medium)' || iconOnlyHeight !== iconOnlyWidth || sendRadius !== '50%') {
throw new Error(`Unexpected REA send-button sizing: height ${sendSize}, button width ${iconOnlyWidth}, height ${iconOnlyHeight}`)
}
if (declaration(sendButtonRule, '--wb-button-icon-size-medium') !== '32px' || declaration(sendIconRule, 'width') !== '32px') {
throw new Error('REA send-button icon sizing changed; review the native-button projection before regenerating')
}
const sourceNotes = [
`CSS Evidence ID ${evidenceId}`,
`capture target ${manifest.target?.title ?? 'unknown'}; provider ${manifest.provider?.id ?? 'unknown'}`,
...[...sources.values()].map((source) => `${source.file} sha256=${source.sha256}`),
'Only selected input-area rules are recovered; source sheets are not imported wholesale.',
'WorkBuddy input-wrapper wb-color tokens use the recovered scoped theme declarations.',
'The native textarea receives declarations selected from .cr-input-editor-host [data-slate-editor]; no Slate data attributes are added.',
'The native send button receives the required .wb-button base, icon-only, medium, and plain declarations as a local projection.'
]
const cssSections = [
`/*\n * Generated by scripts/rea/recover-composer-styles.mjs.\n${sourceNotes.map((note) => ` * ${note}`).join('\n')}\n */`,
...sourceRules.map((record) => formatRule(
record.selectors.join(', '),
record.declarations,
{ file: record.source.file, column: record.column },
record.source.sha256
)),
formatRule(
'.cr-input-container:focus-within',
declarationText(focusedContainerRule),
{ file: cy.file, column: focusedContainerRule.source?.start?.column ?? 1 },
cy.sha256,
'projected from the original focus state class for a native focusable container'
),
formatRule(
'.conversation-input__box .cr-input-container:focus-within',
declarationText(findRule(contexts.root, '.conversation-input__box .cr-input-container--focused')),
{ file: contexts.file, column: findRule(contexts.root, '.conversation-input__box .cr-input-container--focused').source?.start?.column ?? 1 },
contexts.sha256,
'selector projection of WorkBuddy focus-border class'
),
formatRule(
'.chat-pane[data-cr-theme=dark] .conversation-input__box .cr-input-container:focus-within',
declarationText(findRule(contexts.root, '.vscode-dark .conversation-input__box .cr-input-container--focused')),
{ file: contexts.file, column: findRule(contexts.root, '.vscode-dark .conversation-input__box .cr-input-container--focused').source?.start?.column ?? 1 },
contexts.sha256,
'dark theme selector projection'
),
formatRule(
'.cr-input-editor-host > .wb-composer__input',
declarationText(slateEditorRule),
{ file: cy.file, column: slateEditorRule.source?.start?.column ?? 1 },
cy.sha256,
'selector projection from [data-slate-editor] to the native textarea; attribute intentionally omitted'
),
formatRule(
'.wb-composer__add-button.cr-input-footer-item',
[
`width: ${footerItemHeight};`,
`min-width: ${footerItemHeight};`,
'padding: 0;'
],
{ file: cy.file, column: footerItemRule.source?.start?.column ?? 1 },
cy.sha256,
'icon-only projection retains the original 32px footer-item height'
),
`/* Native button adapter from ${button.file} and ${sources.get('tokens-Df-i-zsO.css').file}; .wb-button base/icon-only/plain/circle declarations, resolved medium height ${sendSize}, border radius ${sendRadius}. */\n.cr-send-button {\n${[
'box-sizing',
'cursor',
'white-space',
'user-select',
'border',
'justify-content',
'align-items',
'font-family',
'font-weight',
'text-decoration',
'display'
].map((property) => ` ${property}: ${declaration(nativeButtonRule, property)};`).join('\n')}\n width: ${sendSize};\n min-width: ${declaration(iconOnlyRule, 'min-width')};\n height: ${sendSize};\n padding: ${declaration(iconOnlyRule, 'padding')};\n border-radius: ${sendRadius};\n color: ${declaration(plainButtonRule, 'color')};\n background-color: ${declaration(plainButtonRule, 'background-color')};\n}`
]
const output = `${cssSections.join('\n\n')}\n`
postcss.parse(output, { from: relative(rootDir, outputPath) })
await writeFile(outputPath, output, 'utf8')
console.log(`Wrote ${relative(rootDir, outputPath)} from ${sources.size} REA CSS exports (${evidenceId}).`)