Files
ycode/scripts/rea/capture-ycode-message-parity.mjs
T

1221 lines
55 KiB
JavaScript

import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import fs from 'node:fs/promises'
import path from 'node:path'
import { createRequire } from 'node:module'
import { spawn } from 'node:child_process'
import { pathToFileURL } from 'node:url'
import react from '@vitejs/plugin-react'
import { createServer } from 'vite'
const require = createRequire(import.meta.url)
const projectRoot = process.cwd()
const rendererRoot = path.join(projectRoot, 'src/renderer')
const runtimeRoot = path.join(projectRoot, '.tmp/rea-css-runtime/dist')
const sourceFlowEvidenceId = 'ev_fc29c47ef7b284267584efe1fcabe48345e37a0b03cf492b686faec7aa704324'
const sourceFlowCssPath = path.join(projectRoot, '.tmp/rea-css-recovered/src-CyXHA0IC.css')
const electronExecutable = require('electron')
const hash = bytes => createHash('sha256').update(bytes).digest('hex')
const captureModes = {
'offscreen-disabled-gpu': {
id: 'offscreen-disabled-gpu',
offscreen: true,
disableHardwareAcceleration: true,
disableGpuSwitch: true,
captureOptions: null,
},
'hidden-hardware': {
id: 'hidden-hardware',
offscreen: false,
disableHardwareAcceleration: false,
disableGpuSwitch: false,
captureOptions: { stayHidden: true, stayAwake: true },
},
'hidden-debugger': {
id: 'hidden-debugger',
offscreen: false,
disableHardwareAcceleration: false,
disableGpuSwitch: false,
debuggerScreenshot: true,
backgroundThrottling: false,
},
'visible-debugger': {
id: 'visible-debugger',
offscreen: false,
disableHardwareAcceleration: false,
disableGpuSwitch: false,
debuggerScreenshot: true,
backgroundThrottling: false,
showInactiveBeforeCapture: true,
closeAfterCapture: true,
},
'visible-swiftshader': {
id: 'visible-swiftshader',
offscreen: false,
disableHardwareAcceleration: true,
disableGpuSwitch: false,
debuggerScreenshot: true,
backgroundThrottling: false,
showInactiveBeforeCapture: true,
closeAfterCapture: true,
graphicsSwitches: ['--use-angle=swiftshader', '--disable-gpu-compositing'],
},
}
const electronHelperSource = String.raw`
const fs = require('node:fs')
const path = require('node:path')
const zlib = require('node:zlib')
const { app, BrowserWindow, nativeImage } = require('electron')
const jobPath = process.env.YCODE_MESSAGE_PARITY_JOB
const resultPath = process.env.YCODE_MESSAGE_PARITY_RESULT
const logPath = process.env.YCODE_MESSAGE_PARITY_LOG
const mark = value => { try { fs.appendFileSync(logPath, String(Date.now()) + ' ' + value + '\n') } catch {} }
const finish = (value, code = 0) => {
mark('finish:' + code)
try { fs.writeFileSync(resultPath, JSON.stringify(value)) } catch {}
app.exit(code)
}
const fail = error => finish({ ok: false, error: String(error && (error.stack || error.message) || error) }, 1)
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
mark('main-module-start')
async function captureGpuState(job) {
const gpuInfoDetail = 'complete'
let gpuFeatureStatus = null
try { gpuFeatureStatus = app.getGPUFeatureStatus() } catch (error) { gpuFeatureStatus = { error: String(error.message || error) } }
let gpuInfo = null
try {
gpuInfo = await Promise.race([
app.getGPUInfo(gpuInfoDetail).catch(error => ({ error: String(error.message || error) })),
delay(4000).then(() => ({ unavailable: 'timeout' })),
])
} catch (error) { gpuInfo = { error: String(error.message || error) } }
return {
mode: job.captureMode.id,
offscreen: job.captureMode.offscreen,
hardwareAccelerationDisabledByApp: job.captureMode.disableHardwareAcceleration,
disableGpuSwitchRequested: job.captureMode.disableGpuSwitch,
disableGpuSwitchPresent: app.commandLine.hasSwitch('disable-gpu'),
backgroundThrottlingDisabled: job.captureMode.backgroundThrottling === false,
showInactiveBeforeCapture: job.captureMode.showInactiveBeforeCapture === true,
windowVisibleAtCapture: false,
windowClosedAfterCapture: false,
chromiumVersion: process.versions.chrome || null,
electronVersion: process.versions.electron || null,
gpuFeatureStatus,
gpuInfoDetail,
gpuInfo,
captureMethod: job.captureMode.debuggerScreenshot
? 'webContents.debugger.sendCommand(Page.captureScreenshot, { format: png, fromSurface: true, captureBeyondViewport: false })'
: job.captureMode.captureOptions
? 'webContents.capturePage(undefined, { stayHidden: true, stayAwake: true })'
: 'webContents.capturePage()',
}
}
function crc32(bytes) {
let crc = 0xffffffff
for (const byte of bytes) {
crc ^= byte
for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0)
}
return (crc ^ 0xffffffff) >>> 0
}
function pngChunk(type, payload) {
const typeBytes = Buffer.from(type, 'ascii')
const length = Buffer.alloc(4)
length.writeUInt32BE(payload.length)
const checksum = Buffer.alloc(4)
checksum.writeUInt32BE(crc32(Buffer.concat([typeBytes, payload])))
return Buffer.concat([length, typeBytes, payload, checksum])
}
function detectBitmapChannelOrder() {
const header = Buffer.alloc(13)
header.writeUInt32BE(1, 0)
header.writeUInt32BE(1, 4)
header[8] = 8
header[9] = 6
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
const pixel = zlib.deflateSync(Buffer.from([0, 17, 83, 201, 255]))
const png = Buffer.concat([signature, pngChunk('IHDR', header), pngChunk('IDAT', pixel), pngChunk('IEND', Buffer.alloc(0))])
const image = nativeImage.createFromBuffer(png)
if (image.isEmpty()) throw new Error('nativeImage could not decode the channel-order calibration PNG')
const bitmap = image.toBitmap({ scaleFactor: 1 })
if (bitmap.length !== 4 || bitmap[3] !== 255) throw new Error('nativeImage returned invalid channel-order calibration pixels')
if (bitmap[0] === 201 && bitmap[1] === 83 && bitmap[2] === 17) return { name: 'BGRA', red: 2, green: 1, blue: 0, alpha: 3 }
if (bitmap[0] === 17 && bitmap[1] === 83 && bitmap[2] === 201) return { name: 'RGBA', red: 0, green: 1, blue: 2, alpha: 3 }
throw new Error('nativeImage bitmap channel order did not match the calibration PNG: ' + Array.from(bitmap).join(','))
}
function analyzePixelRegion(pngBytes, region, channelOrder) {
const image = nativeImage.createFromBuffer(pngBytes)
if (image.isEmpty()) throw new Error('nativeImage could not decode a pixel-analysis PNG')
const size = image.getSize()
const { x, y, width, height } = region
if (x < 0 || y < 0 || width < 1 || height < 1 || x + width > size.width || y + height > size.height) {
throw new Error('Pixel-analysis region falls outside its PNG')
}
const bitmap = image.toBitmap({ scaleFactor: 1 })
if (bitmap.length !== size.width * size.height * 4) throw new Error('nativeImage returned an unexpected pixel-analysis bitmap size')
const colorAt = (offset) => ({
r: bitmap[offset + channelOrder.red],
g: bitmap[offset + channelOrder.green],
b: bitmap[offset + channelOrder.blue],
a: bitmap[offset + channelOrder.alpha],
})
const histogram = new Map()
const pixels = []
for (let py = y; py < y + height; py += 1) {
for (let px = x; px < x + width; px += 1) {
const color = colorAt((py * size.width + px) * 4)
if (color.a === 0) continue
const key = color.r + ',' + color.g + ',' + color.b
histogram.set(key, (histogram.get(key) ?? 0) + 1)
pixels.push(color)
}
}
const backgroundEntry = [...histogram.entries()].sort((left, right) => right[1] - left[1])[0]
const background = backgroundEntry ? backgroundEntry[0].split(',').map(Number) : [0, 0, 0]
const luminance = color => 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b
const backgroundColor = { r: background[0], g: background[1], b: background[2] }
const backgroundLuminance = luminance(backgroundColor)
const orderCounts = { 'R>G>B': 0, 'R>B>G': 0, 'G>R>B': 0, 'G>B>R': 0, 'B>R>G': 0, 'B>G>R': 0, tied: 0 }
const chromaticEdges = new Map()
let inkPixels = 0
let edgePixels = 0
let chromaticEdgePixels = 0
let darkest = null
for (const color of pixels) {
const lightness = luminance(color)
const contrastFromBackground = backgroundLuminance - lightness
if (contrastFromBackground > 24) {
inkPixels += 1
if (!darkest || lightness < darkest.luminance) darkest = { color: { r: color.r, g: color.g, b: color.b }, luminance: lightness }
}
if (contrastFromBackground > 8 && contrastFromBackground < 210) {
edgePixels += 1
const max = Math.max(color.r, color.g, color.b)
const min = Math.min(color.r, color.g, color.b)
if (max - min >= 2) {
chromaticEdgePixels += 1
const sorted = [['R', color.r], ['G', color.g], ['B', color.b]].sort((left, right) => right[1] - left[1])
const key = sorted[0][1] === sorted[1][1] || sorted[1][1] === sorted[2][1]
? 'tied'
: sorted.map(entry => entry[0]).join('>')
orderCounts[key] += 1
const rgb = color.r + ',' + color.g + ',' + color.b
chromaticEdges.set(rgb, (chromaticEdges.get(rgb) ?? 0) + 1)
}
}
}
return {
region,
dimensions: size,
pixelFormat: channelOrder.name,
modalBackground: { ...backgroundColor, pixels: backgroundEntry?.[1] ?? 0 },
darkestInkPixel: darkest,
inkCandidatePixels: inkPixels,
edgeCandidatePixels: edgePixels,
chromaticEdgePixels,
chromaticEdgeOrderCounts: orderCounts,
commonChromaticEdgeRgb: [...chromaticEdges.entries()]
.sort((left, right) => right[1] - left[1])
.slice(0, 12)
.map(([rgb, count]) => ({ rgb: rgb.split(',').map(Number), count })),
}
}
function analyzeReferenceAndCandidate(job, candidateBytes) {
const referenceBytes = fs.readFileSync(job.referencePng)
const channelOrder = detectBitmapChannelOrder()
const regions = Object.fromEntries(Object.entries(job.pixelRegions).map(([name, region]) => [name, {
reference: analyzePixelRegion(referenceBytes, region, channelOrder),
candidate: analyzePixelRegion(candidateBytes, region, channelOrder),
}]))
return { method: 'Electron nativeImage decoded 8-bit bitmap; channel order calibrated against known RGBA PNG', channelOrder: channelOrder.name, regions }
}
async function waitUntil(predicate, timeoutMs, label) {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
if (await predicate()) return
await delay(100)
}
throw new Error('Timed out waiting for ' + label)
}
async function closeAndWait(win) {
if (!win || win.isDestroyed()) return true
const closed = new Promise(resolve => win.once('closed', () => resolve(true)))
win.close()
return Promise.race([closed, delay(2500).then(() => win.isDestroyed())])
}
function alignTargets(entries) {
const result = {}
for (const entry of entries) {
const host = document.querySelector(entry.hostSelector)
const target = host?.querySelector(entry.targetSelector)
if (!host || !target) throw new Error('Missing fixture node for ' + entry.name)
const rect = target.getBoundingClientRect()
const before = { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
const hostRect = host.getBoundingClientRect()
const beforeHost = { x: hostRect.x, y: hostRect.y, width: hostRect.width, height: hostRect.height }
const deltaY = entry.referenceBounds.y - rect.y
host.style.top = (parseFloat(host.style.top) + deltaY) + 'px'
result[entry.name] = {
referenceBounds: entry.referenceBounds,
beforeAlignmentBounds: before,
outerHostBefore: beforeHost,
outerHostTopDelta: deltaY,
anchorMethod: 'outer fixture top adjusted from measured target y; component fonts and intrinsic dimensions untouched',
}
}
return result
}
function measureFixture() {
const rectOf = node => {
const rect = node.getBoundingClientRect()
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, right: rect.right, bottom: rect.bottom }
}
const styleOf = node => {
const style = getComputedStyle(node)
return {
display: style.display,
position: style.position,
fontFamily: style.fontFamily,
fontSize: style.fontSize,
fontWeight: style.fontWeight,
lineHeight: style.lineHeight,
letterSpacing: style.letterSpacing,
width: style.width,
top: style.top,
right: style.right,
bottom: style.bottom,
left: style.left,
transform: style.transform,
webkitFontSmoothing: style.getPropertyValue('-webkit-font-smoothing'),
mozOsxFontSmoothing: style.getPropertyValue('-moz-osx-font-smoothing'),
textRendering: style.textRendering,
color: style.color,
backgroundColor: style.backgroundColor,
padding: style.padding,
margin: style.margin,
borderRadius: style.borderRadius,
boxSizing: style.boxSizing,
maxWidth: style.maxWidth,
overflow: style.overflow,
}
}
const describe = node => ({ text: node.textContent, bounds: rectOf(node), computedStyle: styleOf(node) })
const userHost = document.querySelector('[data-parity-fixture="user"]')
const assistantHost = document.querySelector('[data-parity-fixture="assistant"]')
const sourceFlowWrapper = document.querySelector('[data-parity-source-flow]')
const userBubble = userHost?.querySelector('.cr-self-bubble')
const userLeaf = userHost?.querySelector('.yc-user-editable p > .yc-user-leaf > span')
const paragraph = assistantHost?.querySelector('.cr-agent__content .cr-markdown > p:first-child')
if (!userHost || !assistantHost || !userBubble || !userLeaf || !paragraph) throw new Error('Fixture target nodes are not present after rendering')
return {
userBubble: {
outerHost: describe(userHost),
message: describe(userHost.querySelector('.msg--user')),
bubble: describe(userBubble),
text: describe(userLeaf),
},
firstParagraph: {
outerHost: describe(assistantHost),
message: describe(assistantHost.querySelector('.msg--assistant')),
agent: describe(assistantHost.querySelector('.cr-agent')),
paragraph: describe(paragraph),
},
viewport: { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio },
userAgent: navigator.userAgent,
readyFonts: document.fonts.status,
sourceFlowWrapper: sourceFlowWrapper ? describe(sourceFlowWrapper) : null,
fixtureHasFixedTargetFontOrHeight: false,
}
}
try {
const job = JSON.parse(fs.readFileSync(jobPath, 'utf8'))
const dataRoot = path.join(path.dirname(jobPath), 'electron-data')
for (const name of ['userData', 'sessionData', 'logs']) {
const directory = path.join(dataRoot, name)
fs.mkdirSync(directory, { recursive: true })
try { app.setPath(name, directory) } catch {}
}
if (job.captureMode.disableHardwareAcceleration) app.disableHardwareAcceleration()
mark('app-paths-configured')
app.whenReady().then(async () => {
mark('app-ready')
const consoleErrors = []
const failedRequests = []
let win
try {
win = new BrowserWindow({
show: false,
useContentSize: true,
width: job.viewport.width,
height: job.viewport.height,
backgroundColor: '#ffffff',
webPreferences: {
offscreen: job.captureMode.offscreen,
contextIsolation: true,
...(job.captureMode.backgroundThrottling === false ? { backgroundThrottling: false } : {}),
},
})
mark('window-created')
win.webContents.on('console-message', (_event, level, message) => {
if (level === 3) consoleErrors.push(String(message))
})
win.webContents.on('did-fail-load', (_event, code, description, url) => failedRequests.push({ code, description, url }))
win.webContents.on('did-finish-load', () => mark('did-finish-load'))
win.webContents.on('dom-ready', () => mark('dom-ready'))
mark('load-url-start')
await Promise.race([
win.loadURL(job.url),
delay(30000).then(() => { throw new Error('Timed out loading the Vite fixture page') }),
])
mark('load-url-finished')
await waitUntil(() => win.webContents.executeJavaScript(
"document.documentElement.dataset.messageParityReady === 'true'",
), 45000, 'React fixture and document fonts')
mark('fixture-ready')
await win.webContents.executeJavaScript('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))')
mark('initial-render-frames')
const calibration = await win.webContents.executeJavaScript(
'(' + alignTargets.toString() + ')(' + JSON.stringify(job.anchors) + ')',
)
mark('targets-aligned')
await win.webContents.executeJavaScript('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))')
mark('aligned-render-frames')
const measurements = await win.webContents.executeJavaScript('(' + measureFixture.toString() + ')()')
mark('fixture-measured')
let windowVisibleAtCapture = false
if (job.captureMode.showInactiveBeforeCapture) {
win.showInactive()
mark('window-shown-inactive')
await waitUntil(() => win.isVisible(), 5000, 'non-activating fixture window visibility')
await delay(100)
await win.webContents.executeJavaScript('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))')
windowVisibleAtCapture = win.isVisible()
if (!windowVisibleAtCapture) throw new Error('Fixture window was not visible before debugger screenshot')
}
const gpu = await captureGpuState(job)
gpu.windowVisibleAtCapture = windowVisibleAtCapture
gpu.graphicsSwitchesRequested = job.captureMode.graphicsSwitches || []
mark('gpu-state-captured-after-window-presentation')
let candidatePng
let size
if (job.captureMode.debuggerScreenshot) {
const debuggerSession = win.webContents.debugger
if (debuggerSession.isAttached()) throw new Error('Fixture webContents debugger unexpectedly already attached')
debuggerSession.attach('1.3')
mark('debugger-attached')
try {
await debuggerSession.sendCommand('Page.enable')
mark('debugger-page-enabled')
const captured = await Promise.race([
debuggerSession.sendCommand('Page.captureScreenshot', {
format: 'png',
fromSurface: true,
captureBeyondViewport: false,
}),
delay(15000).then(() => { throw new Error('Timed out waiting for Page.captureScreenshot on the hidden fixture window') }),
])
mark('debugger-page-screenshot-captured')
candidatePng = Buffer.from(captured.data, 'base64')
} finally {
if (debuggerSession.isAttached()) debuggerSession.detach()
}
if (job.captureMode.closeAfterCapture && !win.isDestroyed()) {
gpu.windowClosedAfterCapture = await closeAndWait(win)
if (!gpu.windowClosedAfterCapture) throw new Error('Fixture window did not close after debugger screenshot')
mark('window-closed-after-capture')
}
const decoded = nativeImage.createFromBuffer(candidatePng)
if (decoded.isEmpty()) throw new Error('nativeImage could not decode the debugger Page.captureScreenshot result')
size = decoded.getSize()
mark('debugger-png-decoded')
} else {
const image = job.captureMode.captureOptions
? await win.webContents.capturePage(undefined, job.captureMode.captureOptions)
: await win.webContents.capturePage()
size = image.getSize()
candidatePng = image.toPNG()
}
if (size.width !== job.viewport.width || size.height !== job.viewport.height) {
throw new Error('Candidate PNG size is ' + size.width + 'x' + size.height + ', expected '
+ job.viewport.width + 'x' + job.viewport.height)
}
fs.writeFileSync(job.outputPng, candidatePng)
mark('capture-written')
const pixelAnalysis = analyzeReferenceAndCandidate(job, candidatePng)
finish({ ok: true, calibration, measurements, gpu, viewport: size, pixelAnalysis, consoleErrors, failedRequests })
} catch (error) {
if (job.captureMode.closeAfterCapture && win && !win.isDestroyed()) {
await closeAndWait(win)
mark('window-closed-after-failed-capture')
}
finish({ ok: false, error: String(error && (error.stack || error.message) || error), consoleErrors, failedRequests }, 1)
}
}).catch(fail)
} catch (error) {
fail(error)
}
`
function fail(message) {
throw new Error(message)
}
function readJson(file) {
return fs.readFile(file, 'utf8').then(JSON.parse)
}
function findByClass(nodes, className) {
const node = nodes.find(entry => (entry.attributes?.class ?? '').split(/\s+/).includes(className))
if (!node) fail(`REA layout is missing .${className}`)
return node
}
function textWithin(nodes, root) {
const children = new Map()
for (const node of nodes) {
const list = children.get(node.parent_index) ?? []
list.push(node)
children.set(node.parent_index, list)
}
const visit = node => node.node_name === '#text'
? node.text ?? ''
: (children.get(node.node_index) ?? []).map(visit).join('')
return visit(root)
}
function isDescendant(nodes, node, ancestorIndex) {
const byIndex = new Map(nodes.map(entry => [entry.node_index, entry]))
let parent = node.parent_index
while (parent !== undefined && parent !== null && parent >= 0) {
if (parent === ancestorIndex) return true
parent = byIndex.get(parent)?.parent_index
}
return false
}
function integerRegion(bounds, viewport) {
const [rawX, rawY, rawWidth, rawHeight] = bounds
const x = Math.floor(rawX)
const y = Math.floor(rawY)
const right = Math.ceil(rawX + rawWidth)
const bottom = Math.ceil(rawY + rawHeight)
const region = { x, y, width: right - x, height: bottom - y }
if (x < 0 || y < 0 || right > viewport.width || bottom > viewport.height || region.width < 1 || region.height < 1) {
fail(`REA target bounds do not fit in the ${viewport.width}x${viewport.height} viewport`)
}
return region
}
async function authenticateEvidence(raw) {
const moduleUrl = pathToFileURL(path.join(runtimeRoot, 'domain/evidence.js'))
let parseEvidence
try {
({ parseEvidence } = await import(moduleUrl))
} catch (error) {
fail(`Prepared REA runtime is required to authenticate the greeting archive: ${error.message}`)
}
return parseEvidence(raw)
}
async function loadReference(directory) {
const reportPath = path.join(directory, 'font-platform-report.json')
const layoutPath = path.join(directory, 'layout.json')
const screenshotPath = path.join(directory, 'workbuddy.png')
const [report, layout, screenshotBytes] = await Promise.all([
readJson(reportPath),
readJson(layoutPath),
fs.readFile(screenshotPath),
])
if (report.source?.kind !== 'rea-workbuddy') fail('Greeting font report must identify rea-workbuddy as its source')
if (!/^ev_[a-f0-9]{64}$/.test(report.evidenceId ?? '')) fail('Greeting report has an invalid Evidence ID')
if (report.evidenceId !== layout.evidenceId) fail('Greeting layout and font report Evidence IDs differ')
if (report.version === 'unobserved') fail('Greeting report does not record an observed WorkBuddy version')
if (!/^[a-f0-9]{64}$/i.test(report.evidenceSha256 ?? '')) fail('Greeting report has no valid archive SHA-256')
const archivePath = path.resolve(projectRoot, report.archive)
const expectedArchive = path.resolve(projectRoot, '.tmp/rea-css-evidence', `${report.evidenceId}.json`)
if (process.platform === 'win32'
? archivePath.toLowerCase() !== expectedArchive.toLowerCase()
: archivePath !== expectedArchive) fail('Greeting report archive path is not the archive named by its Evidence ID')
if (layout.archive && path.resolve(projectRoot, layout.archive).toLowerCase() !== expectedArchive.toLowerCase()) {
fail('Greeting layout archive does not match the font report Evidence ID')
}
const archiveBytes = await fs.readFile(archivePath)
const evidenceSha256 = hash(archiveBytes)
if (evidenceSha256 !== report.evidenceSha256.toLowerCase()) fail('Greeting archive SHA-256 does not match its font report')
const archive = JSON.parse(archiveBytes.toString('utf8'))
if (archive.evidence_id !== report.evidenceId) fail('Greeting archive Evidence ID does not match its font report')
const evidence = await authenticateEvidence(archive)
if (evidence.operation !== 'inspect_electron_page' || evidence.provider?.id !== 'rea-cdp-electron') {
fail('Greeting archive is not an authenticated REA Electron page observation')
}
const target = evidence.normalized_result?.target?.file_path ?? ''
if (!/WorkBuddy[\\/]resources[\\/]app\.asar[\\/]renderer[\\/]index\.html$/i.test(target)) {
fail('Greeting Evidence target is not the WorkBuddy renderer')
}
if (evidence.normalized_result?.browser?.user_agent?.match(/WorkBuddy[ /]([\d.]+)/i)?.[1] !== report.version) {
fail('Greeting report version does not match the archived WorkBuddy user agent')
}
const archiveVisuals = evidence.normalized_result?.visuals
if (!Array.isArray(archiveVisuals?.font_samples)) fail('Greeting archive has no font sample observation')
assert.deepEqual(report.samples, archiveVisuals.font_samples, 'font report samples differ from authenticated REA archive')
assert.deepEqual(report.missing ?? [], archiveVisuals.font_missing ?? [], 'font report missing samples differ from authenticated REA archive')
if (layout.artifact?.sha256 !== hash(screenshotBytes)) fail('Greeting PNG SHA-256 differs from its layout record')
const archivedScreenshot = archiveVisuals.artifact
if (!archivedScreenshot?.data_base64 || hash(Buffer.from(archivedScreenshot.data_base64, 'base64')) !== layout.artifact.sha256) {
fail('Greeting PNG is not the screenshot stored in authenticated REA Evidence')
}
if (layout.viewport?.width !== 1336 || layout.viewport?.height !== 901) fail('Greeting layout viewport must be 1336x901')
if (!Array.isArray(layout.message_content?.nodes)) fail('Greeting layout has no REA message_content export')
if (!isDeepEqual(report.samples, layout.font_samples)) fail('Greeting font report samples differ from layout font samples')
const nodes = layout.message_content.nodes
const userRoot = findByClass(nodes, 'cr-user-content')
const userLeaf = nodes.find(node => node.node_name === '#text' && isDescendant(nodes, node, userRoot.node_index))
if (!userLeaf) fail('REA message_content has no text leaf under .cr-user-content')
const assistantRoot = findByClass(nodes, 'cr-agent__content')
const firstParagraph = nodes.find(node => node.node_name === 'P' && isDescendant(nodes, node, assistantRoot.node_index))
if (!firstParagraph) fail('REA message_content has no assistant paragraph under .cr-agent__content')
const userText = textWithin(nodes, userRoot)
const assistantText = textWithin(nodes, firstParagraph)
const userSample = report.samples.find(sample => sample.category === 'user-bubble')
const assistantSample = report.samples.find(sample => sample.category === 'chinese-body')
if (!userSample || !assistantSample) fail('Greeting report must contain user-bubble and chinese-body samples')
if (userText !== userSample.text || assistantText !== assistantSample.text) {
fail('REA message_content text differs from the authenticated greeting font sample')
}
const documentNodes = layout.layout?.documents?.flatMap(document => document.nodes) ?? []
const userBubbleNode = findByClass(documentNodes, 'cr-self-bubble')
const agentNode = findByClass(documentNodes, 'cr-agent')
if (!Array.isArray(userBubbleNode.bounds) || !Array.isArray(agentNode.bounds)) fail('Greeting layout is missing target node bounds')
if (!Array.isArray(userSample.bounds) || !Array.isArray(assistantSample.bounds)) fail('Greeting font samples are missing target bounds')
return {
reportPath,
reportSha256: hash(await fs.readFile(reportPath)),
report,
layout,
archivePath,
evidenceSha256,
screenshotPath,
screenshotSha256: hash(screenshotBytes),
userSample,
assistantSample,
userBubbleNode,
agentNode,
userText,
assistantText,
}
}
function isDeepEqual(left, right) {
return JSON.stringify(left) === JSON.stringify(right)
}
async function readSourceFlowBasis(reference) {
if (reference.report.evidenceId !== sourceFlowEvidenceId) {
fail(`--source-flow-transform requires REA Evidence ${sourceFlowEvidenceId}`)
}
const documentNodes = reference.layout.layout?.documents?.flatMap(document => document.nodes) ?? []
const flowNode = findByClass(documentNodes, 'cr-document__virtual-flow')
const userBubble = findByClass(documentNodes, 'cr-self-bubble')
const firstParagraph = documentNodes.find(node => node.node_name === 'P' && isDeepEqual(node.bounds, reference.assistantSample.bounds))
if (!firstParagraph) fail('Source-flow REA layout has no paragraph matching the selected Chinese-body sample')
if (flowNode.styles?.transform !== 'matrix(1, 0, 0, 1, 0, 0)') {
fail('Source-flow REA ancestor transform is not the observed identity matrix')
}
if (!isDescendant(documentNodes, userBubble, flowNode.node_index)
|| !isDescendant(documentNodes, firstParagraph, flowNode.node_index)) {
fail('Source-flow REA ancestor is not shared by the user bubble and sampled paragraph')
}
const cssBytes = await fs.readFile(sourceFlowCssPath)
const cssText = cssBytes.toString('utf8')
const expectedRule = '.cr-document__virtual-flow{width:100%;position:absolute;top:0;left:0}'
if (!cssText.includes(expectedRule)) fail('Recovered REA stylesheet no longer contains the observed virtual-flow positioning rule')
return {
evidenceId: reference.report.evidenceId,
selector: '.cr-document__virtual-flow',
computedTransform: flowNode.styles.transform,
bounds: flowNode.bounds,
sharedBy: ['.cr-self-bubble', 'first sampled chinese-body paragraph'],
recoveredCssPath: path.relative(projectRoot, sourceFlowCssPath).replaceAll('\\', '/'),
recoveredCssSha256: hash(cssBytes),
recoveredCssRule: expectedRule,
}
}
function entrySource(reference, sourceFlowTransform) {
const messages = {
user: {
id: 'rea-greeting-user', role: 'user', content: reference.userText,
createdAt: 0, status: 'done',
},
assistant: {
id: 'rea-greeting-assistant', role: 'assistant', content: reference.assistantText,
parts: [{ type: 'text', text: reference.assistantText }], createdAt: 0, status: 'done',
},
}
const placement = {
x: reference.agentNode.bounds[0],
y: reference.agentNode.bounds[1],
width: reference.assistantSample.bounds[2],
}
const userPlacement = {
x: reference.agentNode.bounds[0],
y: reference.userBubbleNode.bounds[1],
width: reference.assistantSample.bounds[2],
}
const userFixture = `<div data-parity-fixture="user" style={{ position: 'absolute', left: userPosition.x, top: userPosition.y, width: userPosition.width }}><MessageBubble message={messages.user} /></div>`
const assistantFixture = `<div data-parity-fixture="assistant" style={{ position: 'absolute', left: assistantPosition.x, top: assistantPosition.y, width: assistantPosition.width }}><MessageBubble message={messages.assistant} /></div>`
const fixtureNodes = sourceFlowTransform
? `<div data-parity-source-flow style={{ width: '100%', position: 'absolute', inset: 0, transform: 'translateY(0)' }}>${userFixture}${assistantFixture}</div>`
: `${userFixture}${assistantFixture}`
return `import React, { useEffect } from 'react'
import { createRoot } from 'react-dom/client'
import MessageBubble from '@renderer/components/MessageBubble'
import '@renderer/workbuddy/typography/tokens.css'
import '@renderer/styles/tokens.css'
import '@renderer/styles/app.css'
import '@renderer/styles/sidebar.css'
import '@renderer/workbuddy/conversation/source-styles.css'
import '@renderer/workbuddy/conversation/source-adapters.css'
const messages = ${JSON.stringify(messages)}
const assistantPosition = ${JSON.stringify(placement)}
const userPosition = ${JSON.stringify(userPlacement)}
function Fixture() {
useEffect(() => {
document.body.dataset.electronDesktop = 'true'
document.body.dataset.platform = 'windows'
void document.fonts.ready.then(() => requestAnimationFrame(() => requestAnimationFrame(() => {
document.documentElement.dataset.messageParityReady = 'true'
})))
}, [])
return <main className="message-parity-stage" data-parity-stage>
<section className="chat-pane cr-theme" data-cr-theme="light" aria-label="Isolated YCode message components">
${fixtureNodes}
</section>
</main>
}
const stageStyle = document.createElement('style')
stageStyle.textContent = '#root{width:1336px;height:901px}.message-parity-stage{position:relative;width:1336px;height:901px;overflow:hidden;background:#fff}.chat-pane.cr-theme{position:absolute;inset:0;width:100%;height:100%;overflow:hidden;background:#fff}'
document.head.append(stageStyle)
createRoot(document.getElementById('root')).render(<Fixture />)
`
}
async function runElectronHelper(job) {
const helperDirectory = await fs.mkdtemp(path.join(projectRoot, '.tmp/rea-message-parity-helper-'))
const helperPath = path.join(helperDirectory, 'main.cjs')
const packagePath = path.join(helperDirectory, 'package.json')
const jobPath = path.join(helperDirectory, 'job.json')
const resultPath = path.join(helperDirectory, 'result.json')
const logPath = path.join(helperDirectory, 'progress.log')
await fs.writeFile(helperPath, electronHelperSource, 'utf8')
await fs.writeFile(packagePath, JSON.stringify({ name: 'ycode-message-parity-capture', version: '1.0.0', main: 'main.cjs' }), 'utf8')
await fs.writeFile(jobPath, JSON.stringify(job), 'utf8')
const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => name.toUpperCase() !== 'ELECTRON_RUN_AS_NODE'))
env.YCODE_MESSAGE_PARITY_JOB = jobPath
env.YCODE_MESSAGE_PARITY_RESULT = resultPath
env.YCODE_MESSAGE_PARITY_LOG = logPath
try {
const completion = await new Promise((resolve, reject) => {
const electronArgs = ['--no-sandbox', '--disable-extensions']
if (job.captureMode.disableGpuSwitch) electronArgs.push('--disable-gpu')
electronArgs.push(...(job.captureMode.graphicsSwitches || []))
electronArgs.push(helperDirectory)
const child = spawn(electronExecutable, electronArgs, {
cwd: helperDirectory,
env,
windowsHide: true,
stdio: ['ignore', 'ignore', 'pipe'],
})
let stderr = ''
let settled = false
const timer = setTimeout(() => {
child.kill()
if (!settled) {
settled = true
let progress = ''
try { progress = require('node:fs').readFileSync(logPath, 'utf8') } catch {}
reject(new Error(`Hidden Electron message parity capture timed out${progress ? `; helper progress:\n${progress}` : '; helper did not write progress'}${stderr ? `; stderr:\n${stderr.trim()}` : ''}`))
}
}, 60000)
timer.unref()
child.stderr.on('data', chunk => { stderr += chunk.toString() })
child.once('error', error => {
clearTimeout(timer)
if (!settled) { settled = true; reject(error) }
})
child.once('close', (code, signal) => {
clearTimeout(timer)
if (settled) return
settled = true
resolve({ code, signal, stderr })
})
})
let result
try { result = JSON.parse(await fs.readFile(resultPath, 'utf8')) } catch {
throw new Error(`Hidden Electron exited without a result (code ${completion.code}${completion.stderr ? `): ${completion.stderr.trim()}` : ')'}`)
}
if (!result.ok || completion.code !== 0) throw new Error(result.error || `Hidden Electron failed (code ${completion.code})`)
if (result.consoleErrors.length || result.failedRequests.length) {
throw new Error(`Fixture reported renderer errors: ${JSON.stringify({ consoleErrors: result.consoleErrors, failedRequests: result.failedRequests })}`)
}
return result
} finally {
let cleanupError
for (let attempt = 0; attempt < 8; attempt += 1) {
try {
await fs.rm(helperDirectory, { recursive: true, force: true, maxRetries: 2, retryDelay: 150 })
cleanupError = undefined
break
} catch (error) {
cleanupError = error
await new Promise(resolve => setTimeout(resolve, 150))
}
}
if (cleanupError) process.stderr.write(`Temporary Electron helper cleanup failed: ${helperDirectory}: ${cleanupError.message}\n`)
}
}
async function startFixtureServer(reference, sourceFlowTransform) {
const entryPath = path.join(projectRoot, '.tmp', `rea-message-parity-entry-${process.pid}.tsx`)
const htmlPath = path.join(rendererRoot, `__rea-message-parity-${process.pid}.html`)
await Promise.all([entryPath, htmlPath].map(async file => {
try { await fs.access(file); fail(`Refusing to overwrite existing temporary fixture: ${file}`) }
catch (error) { if (error.code !== 'ENOENT') throw error }
}))
const source = entrySource(reference, sourceFlowTransform)
const entryUrl = `/@fs/${encodeURI(entryPath.replaceAll('\\', '/'))}`
const html = `<!doctype html><html class="light"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>YCode message parity fixture</title></head><body class="light agent-ui-theme"><div id="root"></div><script type="module" src="${entryUrl}"></script></body></html>`
await fs.writeFile(entryPath, source, 'utf8')
await fs.writeFile(htmlPath, html, 'utf8')
const server = await createServer({
configFile: false,
root: rendererRoot,
appType: 'mpa',
plugins: [react()],
resolve: {
alias: {
'@renderer': path.join(rendererRoot, 'src'),
'@shared': path.join(projectRoot, 'src/shared'),
},
},
server: {
host: '127.0.0.1',
port: 0,
strictPort: false,
hmr: false,
fs: { allow: [projectRoot] },
},
logLevel: 'error',
})
try {
await server.listen()
const address = server.httpServer?.address()
if (!address || typeof address === 'string') fail('Vite did not expose an HTTP address for the component fixture')
const url = `http://127.0.0.1:${address.port}/__rea-message-parity-${process.pid}.html`
const response = await fetch(url)
if (!response.ok) fail(`Vite fixture health check returned HTTP ${response.status}`)
const htmlText = await response.text()
if (!htmlText.includes(entryUrl)) fail('Vite did not serve the generated fixture HTML')
const entryResponse = await fetch(new URL(entryUrl, url))
if (!entryResponse.ok) fail(`Vite component entry transform returned HTTP ${entryResponse.status}: ${(await entryResponse.text()).slice(0, 500)}`)
return { server, entryPath, htmlPath, url }
} catch (error) {
await server.close()
await Promise.all([entryPath, htmlPath].map(file => fs.rm(file, { force: true })))
throw error
}
}
function boundsArray(box) {
return [box.x, box.y, box.width, box.height]
}
function inspectPng(bytes) {
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
if (bytes.length < 8 || !bytes.subarray(0, 8).equals(signature)) fail('PNG metadata inspection received a non-PNG file')
const chunks = []
let offset = 8
let dimensions
const colorChunkNames = new Set(['iCCP', 'sRGB', 'gAMA', 'cHRM', 'cICP', 'mDCv', 'cLLi'])
while (offset + 12 <= bytes.length) {
const length = bytes.readUInt32BE(offset)
const type = bytes.toString('ascii', offset + 4, offset + 8)
const dataStart = offset + 8
const dataEnd = dataStart + length
const crcEnd = dataEnd + 4
if (crcEnd > bytes.length) fail(`PNG ${type} chunk exceeds its file length`)
const data = bytes.subarray(dataStart, dataEnd)
const chunk = {
type,
bytes: length,
crc32: bytes.readUInt32BE(dataEnd).toString(16).padStart(8, '0'),
}
if (type === 'IHDR' && length === 13) {
dimensions = {
width: data.readUInt32BE(0),
height: data.readUInt32BE(4),
bitDepth: data[8],
colorType: data[9],
compression: data[10],
filter: data[11],
interlace: data[12],
}
}
if (colorChunkNames.has(type)) {
chunk.dataSha256 = hash(data)
if (type === 'iCCP') {
const separator = data.indexOf(0)
chunk.profileName = separator >= 0 ? data.toString('latin1', 0, separator) : null
chunk.compressionMethod = separator >= 0 ? data[separator + 1] : null
} else if (type === 'sRGB') {
chunk.renderingIntent = data[0]
} else if (type === 'gAMA' && data.length === 4) {
chunk.gammaTimes100000 = data.readUInt32BE(0)
} else if (type === 'cHRM' && data.length === 32) {
chunk.chromaticitiesTimes100000 = Array.from({ length: 8 }, (_, index) => data.readUInt32BE(index * 4))
} else if (type === 'cICP' && data.length === 4) {
chunk.cicp = Array.from(data)
}
}
chunks.push(chunk)
offset = crcEnd
if (type === 'IEND') break
}
if (!dimensions || chunks.at(-1)?.type !== 'IEND') fail('PNG is missing a valid IHDR or IEND chunk')
const colorProfileChunks = chunks.filter(chunk => colorChunkNames.has(chunk.type))
return {
sha256: hash(bytes),
bytes: bytes.length,
dimensions,
chunkTypes: chunks.map(chunk => chunk.type),
chunks,
colorProfileChunks,
}
}
function parseArgs(args) {
let outputDirectory = '.tmp/rea-visual-greeting'
let mode = 'offscreen-disabled-gpu'
let suffix = null
let sourceFlowTransform = false
let hasOutputDirectory = false
for (let index = 0; index < args.length; index += 1) {
const value = args[index]
if (value === '--mode' || value === '--capture-mode') {
mode = args[++index]
if (!mode || !captureModes[mode]) fail(`Unsupported capture mode: ${mode ?? 'missing'}`)
} else if (value === '--suffix') {
suffix = args[++index]
if (!suffix || !/^[a-z0-9](?:[a-z0-9-]{0,47})$/i.test(suffix)) fail('Capture suffix must be 1-48 alphanumeric/hyphen characters')
} else if (value === '--source-flow-transform') {
sourceFlowTransform = true
} else if (value === '--help') {
return { help: true }
} else if (value.startsWith('--')) {
fail(`Unknown option: ${value}`)
} else if (!hasOutputDirectory) {
outputDirectory = value
hasOutputDirectory = true
} else {
fail(`Unexpected positional argument: ${value}`)
}
}
return { outputDirectory, captureMode: captureModes[mode], suffix, sourceFlowTransform }
}
async function main() {
const cli = parseArgs(process.argv.slice(2))
if (cli.help) {
console.log('Usage: node scripts/rea/capture-ycode-message-parity.mjs [output-directory] [--mode offscreen-disabled-gpu|hidden-hardware|hidden-debugger|visible-debugger|visible-swiftshader] [--suffix label] [--source-flow-transform]')
return
}
if (cli.sourceFlowTransform && cli.captureMode.id !== 'visible-debugger') {
fail('--source-flow-transform requires --mode visible-debugger')
}
const outputDirectory = path.resolve(cli.outputDirectory)
const captureMode = cli.captureMode
const reference = await loadReference(outputDirectory)
const sourceFlowBasis = cli.sourceFlowTransform ? await readSourceFlowBasis(reference) : null
const viewport = reference.layout.viewport
const userBubbleBounds = reference.userBubbleNode.bounds
const firstParagraphBounds = reference.assistantSample.bounds
const regionUser = integerRegion(userBubbleBounds, viewport)
const regionParagraph = integerRegion(firstParagraphBounds, viewport)
const archiveRel = path.relative(outputDirectory, reference.archivePath).replaceAll('\\', '/')
const referenceEvidence = { id: reference.report.evidenceId, archive: archiveRel }
const outputLabel = cli.suffix ?? (cli.sourceFlowTransform
? `${captureMode.id}-source-flow-transform`
: captureMode.id === 'offscreen-disabled-gpu' ? null : captureMode.id)
const suffix = outputLabel ? `-${outputLabel}` : ''
const outputPng = path.join(outputDirectory, `ycode-message-parity${suffix}.png`)
const layoutReportPath = path.join(outputDirectory, `ycode-message-parity${suffix}-layout.json`)
const manifestPath = path.join(outputDirectory, `ycode-message-parity${suffix}-manifest.json`)
const captureAnalysisPath = path.join(outputDirectory, `ycode-message-parity${suffix}-capture-analysis.json`)
const captureAttemptPath = path.join(outputDirectory, `ycode-message-parity${suffix}-capture-attempt.json`)
const serverHandle = await startFixtureServer(reference, cli.sourceFlowTransform)
try {
let result
try {
result = await runElectronHelper({
url: serverHandle.url,
viewport,
outputPng,
captureMode,
referencePng: reference.screenshotPath,
pixelRegions: { userBubble: regionUser, firstParagraph: regionParagraph },
anchors: [
{
name: 'user-bubble',
hostSelector: '[data-parity-fixture="user"]',
targetSelector: '.cr-self-bubble',
referenceBounds: { x: userBubbleBounds[0], y: userBubbleBounds[1], width: userBubbleBounds[2], height: userBubbleBounds[3] },
},
{
name: 'first-paragraph',
hostSelector: '[data-parity-fixture="assistant"]',
targetSelector: '.cr-agent__content .cr-markdown > p:first-child',
referenceBounds: { x: firstParagraphBounds[0], y: firstParagraphBounds[1], width: firstParagraphBounds[2], height: firstParagraphBounds[3] },
},
],
})
} catch (error) {
if (captureMode.debuggerScreenshot) {
const attempt = {
status: 'incomplete',
reason: 'No candidate PNG was produced; a zero-tolerance pixel comparison cannot be run.',
source: {
kind: 'rea-workbuddy',
version: reference.report.version,
evidenceId: reference.report.evidenceId,
archive: path.relative(projectRoot, reference.archivePath).replaceAll('\\', '/'),
archiveSha256: reference.evidenceSha256,
screenshotSha256: reference.screenshotSha256,
},
capture: {
mode: captureMode.id,
offscreen: captureMode.offscreen,
hardwareAccelerationDisabledByApp: captureMode.disableHardwareAcceleration,
disableGpuSwitchRequested: captureMode.disableGpuSwitch,
showInactiveBeforeCapture: captureMode.showInactiveBeforeCapture === true,
closeAfterCapture: captureMode.closeAfterCapture === true,
captureMethod: 'Page.captureScreenshot({ format: png, fromSurface: true, captureBeyondViewport: false }) on the owned fixture webContents debugger',
},
candidatePath: path.relative(outputDirectory, outputPng).replaceAll('\\', '/'),
error: String(error && (error.stack || error.message) || error),
requiredRegions: { userBubble: regionUser, firstParagraph: regionParagraph },
}
await fs.writeFile(captureAttemptPath, `${JSON.stringify(attempt, null, 2)}\n`)
}
throw error
}
const candidateBytes = await fs.readFile(outputPng)
const candidateSha256 = hash(candidateBytes)
const referenceBytes = await fs.readFile(reference.screenshotPath)
const referencePngInfo = inspectPng(referenceBytes)
const candidatePngInfo = inspectPng(candidateBytes)
const referenceColorChunks = JSON.stringify(referencePngInfo.colorProfileChunks)
const candidateColorChunks = JSON.stringify(candidatePngInfo.colorProfileChunks)
const captureAnalysis = {
scope: 'isolated-component-layout-calibration',
acceptance: 'not-full-app-acceptance',
source: {
kind: 'rea-workbuddy',
evidenceId: reference.report.evidenceId,
archive: path.relative(projectRoot, reference.archivePath).replaceAll('\\', '/'),
archiveSha256: reference.evidenceSha256,
screenshotSha256: reference.screenshotSha256,
},
capture: result.gpu,
png: {
reference: referencePngInfo,
candidate: candidatePngInfo,
comparison: {
sameChunkTypes: JSON.stringify(referencePngInfo.chunkTypes) === JSON.stringify(candidatePngInfo.chunkTypes),
sameColorProfileChunks: referenceColorChunks === candidateColorChunks,
},
},
nativeImagePixels: result.pixelAnalysis,
sourceFlowTransformExperiment: sourceFlowBasis ? {
applied: true,
basis: sourceFlowBasis,
ycodeWrapper: result.measurements.sourceFlowWrapper,
note: 'This applies only the observed identity transform and absolute-flow wrapper; it does not reproduce WorkBuddy virtualization or list positioning.',
} : null,
limitations: [
'Color analysis uses the integer comparison regions and Electron nativeImage decoded bitmaps; the modal region color is used as a background estimate.',
'Chromatic edge counts are diagnostic samples, not proof of LCD subpixel rendering; GPU status is recorded separately.',
'Only the declared user-bubble and first-paragraph regions are analyzed.',
],
}
const rootCommonWidth = reference.assistantSample.bounds[2]
const layoutReport = {
schemaVersion: 1,
scope: 'isolated-component-layout-calibration',
acceptance: 'not-full-app-acceptance',
source: {
kind: 'rea-workbuddy',
version: reference.report.version,
evidenceId: reference.report.evidenceId,
archive: path.relative(projectRoot, reference.archivePath).replaceAll('\\', '/'),
archiveSha256: reference.evidenceSha256,
fontReport: path.relative(projectRoot, reference.reportPath).replaceAll('\\', '/'),
fontReportSha256: reference.reportSha256,
screenshotSha256: reference.screenshotSha256,
},
fixture: {
components: ['MessageBubble', 'MarkdownContent'],
styles: [
'src/renderer/src/workbuddy/typography/tokens.css',
'src/renderer/src/styles/tokens.css',
'src/renderer/src/styles/app.css',
'src/renderer/src/styles/sidebar.css',
'src/renderer/src/workbuddy/conversation/source-styles.css',
'src/renderer/src/workbuddy/conversation/source-adapters.css',
'src/renderer/src/styles/markdown.css (imported by MarkdownContent)',
],
viewport,
environment: {
userAgent: result.measurements.userAgent,
devicePixelRatio: result.measurements.viewport.devicePixelRatio,
fontStatus: result.measurements.readyFonts,
capture: result.gpu,
},
availableWidthPx: rootCommonWidth,
positioning: {
userBubble: result.calibration['user-bubble'],
firstParagraph: result.calibration['first-paragraph'],
widthSource: 'REA assistant content sample bounds; width applied only to outer fixture host',
targetHeightOrFontOverridden: false,
sourceFlowTransformExperiment: sourceFlowBasis ? {
applied: true,
source: sourceFlowBasis,
wrapperStyles: { width: '100%', position: 'absolute', inset: '0px', transform: 'translateY(0)' },
measuredWrapper: result.measurements.sourceFlowWrapper,
targetHeightOrFontOverridden: false,
limitation: 'This models only the observed neutral ancestor transform and positioning rule, not conversation virtualization or its runtime behavior.',
} : null,
},
},
samples: {
userBubble: {
text: reference.userText,
reference: {
bubble: { bounds: userBubbleBounds, computedStyle: reference.userBubbleNode.styles },
text: { bounds: reference.userSample.bounds, computedStyle: reference.userSample.styles },
platformFonts: reference.userSample.platformFonts,
region: regionUser,
},
ycode: result.measurements.userBubble,
},
firstParagraph: {
text: reference.assistantText,
reference: {
bounds: firstParagraphBounds,
computedStyle: reference.assistantSample.styles,
platformFonts: reference.assistantSample.platformFonts,
region: regionParagraph,
},
ycode: result.measurements.firstParagraph,
},
},
candidate: {
path: path.relative(outputDirectory, outputPng).replaceAll('\\', '/'),
sha256: candidateSha256,
bytes: candidateBytes.length,
dimensions: result.viewport,
},
captureAnalysis: path.relative(outputDirectory, captureAnalysisPath).replaceAll('\\', '/'),
comparisonManifest: path.relative(outputDirectory, manifestPath).replaceAll('\\', '/'),
limitations: [
'Only the user bubble and first assistant paragraph are rendered and compared.',
'This isolates real YCode React components and local CSS; it does not assess conversation sequencing, surrounding controls, or full application parity.',
'The fixture aligns each measured target by changing only its outer container top and gives both samples the REA content width. Component height, font-family, font-size, line-height, and weight are not forced.',
'English body and code are absent from this REA greeting sample and remain unverified.',
],
}
const manifest = {
source: { kind: 'rea-workbuddy' },
requiredStates: ['user-bubble', 'first-paragraph'],
channelTolerance: 0,
pairs: [
{
name: 'user-bubble',
reference: path.relative(outputDirectory, reference.screenshotPath).replaceAll('\\', '/'),
candidate: path.relative(outputDirectory, outputPng).replaceAll('\\', '/'),
region: regionUser,
referenceEvidence,
},
{
name: 'first-paragraph',
reference: path.relative(outputDirectory, reference.screenshotPath).replaceAll('\\', '/'),
candidate: path.relative(outputDirectory, outputPng).replaceAll('\\', '/'),
region: regionParagraph,
referenceEvidence,
},
],
}
await Promise.all([
fs.writeFile(layoutReportPath, `${JSON.stringify(layoutReport, null, 2)}\n`),
fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`),
fs.writeFile(captureAnalysisPath, `${JSON.stringify(captureAnalysis, null, 2)}\n`),
])
console.log(JSON.stringify({
scope: layoutReport.scope,
acceptance: layoutReport.acceptance,
version: reference.report.version,
evidenceId: reference.report.evidenceId,
archiveSha256: reference.evidenceSha256,
candidate: { path: path.relative(projectRoot, outputPng).replaceAll('\\', '/'), sha256: candidateSha256, dimensions: result.viewport },
capture: result.gpu,
layoutReport: path.relative(projectRoot, layoutReportPath).replaceAll('\\', '/'),
manifest: path.relative(projectRoot, manifestPath).replaceAll('\\', '/'),
captureAnalysis: path.relative(projectRoot, captureAnalysisPath).replaceAll('\\', '/'),
sourceFlowTransformExperiment: sourceFlowBasis ? {
applied: true,
recoveredCssSha256: sourceFlowBasis.recoveredCssSha256,
computedTransform: sourceFlowBasis.computedTransform,
ycodeWrapperTransform: result.measurements.sourceFlowWrapper?.computedStyle?.transform,
limitation: 'Only the observed ancestor transform is modeled; this is not a virtualization recreation.',
} : null,
pngMetadataComparison: captureAnalysis.png.comparison,
nativeImagePixels: captureAnalysis.nativeImagePixels,
regions: { userBubble: regionUser, firstParagraph: regionParagraph },
ycode: {
userBubble: result.measurements.userBubble,
firstParagraph: result.measurements.firstParagraph,
},
}, null, 2))
} finally {
await serverHandle.server.close()
await Promise.all([serverHandle.entryPath, serverHandle.htmlPath].map(file => fs.rm(file, { force: true })))
}
}
main().catch(error => {
process.stderr.write(`${error.stack || error.message || error}\n`)
process.exitCode = 1
})