Files
ycode/scripts/rea/verify-font-platform-parity.mjs
T

582 lines
29 KiB
JavaScript

import { createRequire } from 'node:module'
import { spawn } from 'node:child_process'
import { createServer } from 'node:net'
import { createHash } from 'node:crypto'
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { isDeepStrictEqual } from 'node:util'
import os from 'node:os'
import path from 'node:path'
import { setTimeout as delay } from 'node:timers/promises'
const require = createRequire(import.meta.url)
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
const referencePath = path.resolve(projectRoot, process.argv[2] ?? '.tmp/rea-font-platform-report.json')
const resultPath = path.resolve(projectRoot, process.argv[3] ?? '.tmp/ycode-font-platform-report.json')
const requiredCategories = ['assistant-name', 'chinese-body', 'english-body', 'user-bubble', 'code']
function fail(message) {
throw new Error(message)
}
function categoryOf(value) {
const key = String(value ?? '').toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, '')
if (/assistant.*name|agent.*name|助手名称|助手名/.test(key)) return 'assistant-name'
if (/chinese.*body|body.*chinese|zh.*body|中文正文|中文文本/.test(key)) return 'chinese-body'
if (/english.*body|body.*english|en.*body|英文正文|英文文本/.test(key)) return 'english-body'
if (/user.*bubble|bubble.*user|用户气泡|用户消息/.test(key)) return 'user-bubble'
if (/code|代码块|代码/.test(key)) return 'code'
return null
}
function normalizeFonts(fonts, label) {
if (!Array.isArray(fonts) || fonts.length === 0) fail(`${label}: platformFonts is empty; no actual glyph usage was recorded`)
return fonts.map((font) => {
const familyName = font.familyName ?? font.family ?? font.name
const postScriptName = font.postScriptName ?? font.postscriptName ?? font.postScript ?? null
const isCustomFont = font.isCustomFont ?? font.customFont ?? font.custom ?? false
const glyphCount = Number(font.glyphCount ?? font.glyphs)
if (!familyName || !Number.isInteger(glyphCount) || glyphCount < 0) {
fail(`${label}: each platform font needs familyName and an integer glyphCount`)
}
return { familyName: String(familyName), postScriptName: postScriptName == null ? null : String(postScriptName), isCustomFont: Boolean(isCustomFont), glyphCount }
}).sort(compareFonts)
}
function compareFonts(left, right) {
return `${left.familyName}\0${left.postScriptName ?? ''}\0${left.isCustomFont}\0${left.glyphCount}`
.localeCompare(`${right.familyName}\0${right.postScriptName ?? ''}\0${right.isCustomFont}\0${right.glyphCount}`)
}
function canonicalJson(value) {
if (value === null || typeof value !== 'object') return JSON.stringify(value)
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
const entries = Object.keys(value).filter((key) => value[key] !== undefined).sort()
return `{${entries.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`
}
function computedEvidenceId(evidence) {
const semantic = {
subject: evidence.subject === null ? null : {
digest: evidence.subject.digest,
format: evidence.subject.format,
architecture: evidence.subject.architecture
},
provider: evidence.provider,
analysis_profile: evidence.analysis_profile,
predicate_type: evidence.predicate_type,
operation: evidence.operation,
parameters: evidence.parameters,
raw_result: evidence.raw_result,
normalized_result: evidence.normalized_result,
confidence: evidence.confidence,
authority: evidence.authority,
environment: evidence.environment,
limitations: evidence.limitations,
...(evidence.relationship_coverage === null ? {} : { relationship_coverage: evidence.relationship_coverage }),
locations: evidence.locations,
evidence_links: evidence.evidence_links
}
return `ev_${createHash('sha256').update(canonicalJson(semantic)).digest('hex')}`
}
function normalizeReference(report) {
const kind = report.source?.kind ?? report.kind
if (!['rea-workbuddy', 'ycode-electron-self-test'].includes(kind)) {
fail(`Unsupported font report source kind: ${kind ?? 'missing'}`)
}
const samples = report.samples ?? report.fontSamples ?? report.result?.samples
if (!Array.isArray(samples)) fail('REA report needs a samples array')
const records = new Map()
const missing = new Map()
for (const sample of samples) {
const category = categoryOf(sample.category ?? sample.kind ?? sample.id ?? sample.label)
if (!category) continue
if (records.has(category)) fail(`REA report contains more than one ${category} sample`)
const text = sample.text ?? sample.content ?? sample.textContent
if (typeof text !== 'string' || text.length === 0) fail(`REA ${category} sample has no non-empty text`)
const codeContext = category === 'code' ? sample.codeContext ?? sample.context ?? 'fenced' : null
if (category === 'code' && !['inline', 'fenced'].includes(codeContext)) {
fail(`REA code codeContext must be inline or fenced, received ${String(codeContext)}`)
}
records.set(category, {
category,
text,
fontFamily: sample.fontFamily ?? sample.computedFontFamily ?? sample.computed?.fontFamily ?? sample.styles?.['font-family'] ?? null,
fontSize: sample.fontSize ?? sample.computed?.fontSize ?? sample.styles?.['font-size'] ?? null,
fontWeight: sample.fontWeight ?? sample.computed?.fontWeight ?? sample.styles?.['font-weight'] ?? null,
codeContext,
platformFonts: normalizeFonts(sample.platformFonts ?? sample.fonts, `REA ${category}`)
})
}
for (const entry of report.missing ?? report.missingSamples ?? []) {
const category = categoryOf(entry.category ?? entry.kind ?? entry.id ?? entry.label)
if (!category) continue
if (records.has(category) || missing.has(category)) fail(`REA report lists ${category} more than once`)
if (typeof entry.reason !== 'string' || !entry.reason.trim()) fail(`REA missing ${category} entry needs a reason`)
missing.set(category, { category, reason: entry.reason })
}
const undeclared = requiredCategories.filter((category) => !records.has(category) && !missing.has(category))
if (undeclared.length) fail(`REA report must explicitly sample or mark missing: ${undeclared.join(', ')}`)
const evidenceId = report.evidenceId ?? report.evidence?.id ?? report.source?.evidenceId ?? report.reference?.evidenceId
const version = report.version ?? report.workbuddyVersion ?? report.source?.version ?? report.reference?.version
if (!evidenceId || !version) fail('REA report must record WorkBuddy version and Evidence ID')
return {
kind,
evidenceId: String(evidenceId),
version: String(version),
environment: report.environment ?? report.runtime ?? report.source?.environment ?? null,
samples: requiredCategories.filter((category) => records.has(category)).map((category) => records.get(category)),
missing: requiredCategories.filter((category) => missing.has(category)).map((category) => missing.get(category))
}
}
async function validateReferenceReport(report, referencePath) {
const reference = normalizeReference(report)
if (reference.kind === 'ycode-electron-self-test') {
if (report.archive || report.evidenceSha256) fail('A local YCode self-test must not claim a REA archive')
return { ...reference, archive: null, archiveSha256: null, archiveVerified: false, versionObserved: true }
}
if (!/^ev_[a-f0-9]{64}$/.test(reference.evidenceId)) fail('REA report has an invalid Evidence ID')
if (typeof report.archive !== 'string' || !report.archive) fail('REA report is missing its archived Evidence path')
if (!/^[a-f0-9]{64}$/i.test(report.evidenceSha256 ?? '')) fail('REA report is missing a valid evidenceSha256')
const archivePath = path.resolve(projectRoot, report.archive)
const expectedArchivePath = path.resolve(projectRoot, '.tmp/rea-css-evidence', `${reference.evidenceId}.json`)
const archivePathKey = process.platform === 'win32' ? archivePath.toLowerCase() : archivePath
const expectedArchivePathKey = process.platform === 'win32' ? expectedArchivePath.toLowerCase() : expectedArchivePath
if (archivePathKey !== expectedArchivePathKey) fail('REA archive path does not match the Evidence ID archive')
const archiveBytes = await readFile(archivePath).catch(() => null)
if (!archiveBytes) fail(`REA Evidence archive not found: ${archivePath}`)
const archiveSha256 = createHash('sha256').update(archiveBytes).digest('hex')
if (archiveSha256 !== report.evidenceSha256.toLowerCase()) fail('REA archive SHA-256 does not match evidenceSha256')
let evidence
try { evidence = JSON.parse(archiveBytes.toString('utf8')) } catch { fail('REA Evidence archive is not valid JSON') }
if (evidence.evidence_id !== reference.evidenceId) fail('REA archive Evidence ID does not match the report')
if (computedEvidenceId(evidence) !== evidence.evidence_id) fail('REA archive Evidence ID does not match its semantic content')
const expectedAuthority = evidence.parameters?.visual_probe?.actions?.length ? 'controlled-replay' : 'external-service'
if (evidence.operation !== 'inspect_electron_page' || evidence.authority !== expectedAuthority || evidence.provider?.id !== 'rea-cdp-electron') {
fail('REA archive is not an authorized Electron page observation')
}
const targetPath = evidence.normalized_result?.target?.file_path ?? ''
if (!/workbuddy[\\/]resources[\\/]app\.asar[\\/]renderer[\\/]index\.html/i.test(targetPath)) {
fail('REA archive target is not the WorkBuddy renderer')
}
const visuals = evidence.normalized_result?.visuals
if (!visuals || !Array.isArray(visuals.font_samples)) fail('REA archive has no font_samples observation')
if (!isDeepStrictEqual(report.samples, visuals.font_samples)) fail('REA report samples differ from archived font_samples')
if (!isDeepStrictEqual(report.missing ?? [], visuals.font_missing ?? [])) fail('REA report missing samples differ from archived font_missing')
const userAgent = evidence.normalized_result?.browser?.user_agent ?? ''
const archiveVersion = userAgent.match(/WorkBuddy[ /]([\d.]+)/i)?.[1] ?? 'unobserved'
if (reference.version !== archiveVersion) fail(`REA report version ${reference.version} does not match archived user agent version ${archiveVersion}`)
return {
...reference,
archive: archivePath,
archiveSha256,
archiveVerified: true,
versionObserved: reference.version.toLowerCase() !== 'unobserved'
}
}
async function reservePort() {
const server = createServer()
await new Promise((resolve, reject) => server.once('error', reject).listen(0, '127.0.0.1', resolve))
const address = server.address()
if (!address || typeof address === 'string') fail('Could not reserve an Electron debugging port')
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))
return address.port
}
function connectCdp(url) {
const socket = new WebSocket(url)
const pending = new Map()
let nextId = 0
const opened = new Promise((resolve, reject) => {
socket.addEventListener('open', resolve, { once: true })
socket.addEventListener('error', () => reject(new Error('Could not connect to local Electron DevTools Protocol')), { once: true })
})
socket.addEventListener('message', (event) => {
let message
try { message = JSON.parse(String(event.data)) } catch { return }
if (!message.id) return
const request = pending.get(message.id)
if (!request) return
clearTimeout(request.timer)
pending.delete(message.id)
if (message.error) request.reject(new Error(`${request.method}: ${message.error.message}`))
else request.resolve(message.result)
})
socket.addEventListener('close', () => {
for (const request of pending.values()) {
clearTimeout(request.timer)
request.reject(new Error('Local Electron DevTools connection closed'))
}
pending.clear()
})
return {
opened,
send(method, params = {}) {
const id = ++nextId
socket.send(JSON.stringify({ id, method, params }))
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id)
reject(new Error(`Timed out waiting for ${method}`))
}, 15000)
pending.set(id, { method, resolve, reject, timer })
})
},
close() { socket.close() }
}
}
async function waitForDevtools(port, child) {
const deadline = Date.now() + 30000
let lastError
while (Date.now() < deadline) {
if (child.exitCode !== null) fail(`YCode Electron exited early with code ${child.exitCode}`)
try {
const response = await fetch(`http://127.0.0.1:${port}/json/version`)
if (response.ok) return await response.json()
} catch (error) { lastError = error }
await delay(250)
}
fail(`YCode Electron did not expose DevTools on port ${port}${lastError ? `: ${lastError.message}` : ''}`)
}
async function waitForPageTarget(port, child) {
const deadline = Date.now() + 30000
while (Date.now() < deadline) {
if (child.exitCode !== null) fail(`YCode Electron exited early with code ${child.exitCode}`)
try {
const response = await fetch(`http://127.0.0.1:${port}/json/list`)
if (response.ok) {
const targets = await response.json()
const target = targets.find((item) => item.type === 'page' && item.webSocketDebuggerUrl)
if (target) return target
}
} catch { /* Electron is still starting. */ }
await delay(250)
}
fail('YCode Electron did not create a renderer page target')
}
function buildProbeExpression(samples) {
const serialized = JSON.stringify(samples.map(({ category, text, codeContext }) => ({ category, text, codeContext })))
return `(() => {
const samples = ${serialized};
const root = document.createElement('section');
root.id = 'yc-font-platform-probe';
root.className = 'chat-pane cr-theme';
root.dataset.crTheme = 'light';
Object.assign(root.style, { position: 'fixed', left: '-20000px', top: '0', width: '900px', zIndex: '-1', pointerEvents: 'none' });
const add = (category, className, parent) => {
const node = document.createElement('span');
node.dataset.fontCategory = category;
node.className = className;
node.textContent = samples.find((item) => item.category === category).text;
parent.append(node);
};
for (const item of samples) {
const message = document.createElement('div');
message.className = item.category === 'user-bubble' ? 'msg--user cr-frame cr-frame--right' : 'msg--assistant cr-frame cr-frame--left';
const frame = document.createElement('div');
frame.className = 'cr-frame__content';
message.append(frame);
if (item.category === 'assistant-name') {
const agent = document.createElement('div');
agent.className = 'cr-agent';
const header = document.createElement('div');
header.className = 'msg__header cr-agent__header';
const name = document.createElement('div');
name.dataset.fontCategory = item.category;
name.className = 'msg__name cr-agent__name';
name.textContent = item.text;
header.append(name);
agent.append(header);
frame.append(agent);
} else if (item.category === 'chinese-body' || item.category === 'english-body') {
const agent = document.createElement('div');
agent.className = 'cr-agent';
const row = document.createElement('div');
row.className = 'cr-agent__body-row';
const body = document.createElement('div');
body.className = 'cr-agent__body';
const content = document.createElement('div');
content.className = 'cr-agent__content markdown-content';
const paragraph = document.createElement('p');
paragraph.dataset.fontCategory = item.category;
paragraph.className = 'cr-text-block';
paragraph.textContent = item.text;
content.append(paragraph);
body.append(content);
row.append(body);
agent.append(row);
frame.append(agent);
} else if (item.category === 'user-bubble') {
const self = document.createElement('div');
self.className = 'cr-self-message';
const content = document.createElement('div');
content.className = 'cr-self-message__content';
const bubble = document.createElement('div');
bubble.className = 'bubble cr-self-bubble';
const userContent = document.createElement('div');
userContent.className = 'cr-user-content';
const editable = document.createElement('div');
editable.className = 'yc-user-editable';
const paragraph = document.createElement('p');
const leaf = document.createElement('span');
leaf.className = 'yc-user-leaf';
add(item.category, '', leaf);
paragraph.append(leaf);
editable.append(paragraph);
userContent.append(editable);
bubble.append(userContent);
content.append(bubble);
self.append(content);
frame.append(self);
} else if (item.category === 'code' && item.codeContext === 'inline') {
const markdown = document.createElement('div');
markdown.className = 'cr-markdown markdown-content';
const list = document.createElement('ul');
const listItem = document.createElement('li');
const code = document.createElement('code');
code.dataset.fontCategory = item.category;
code.className = 'markdown-content__inline-code cr-inline-code';
code.textContent = item.text;
listItem.append(code);
list.append(listItem);
markdown.append(list);
frame.append(markdown);
} else {
const markdown = document.createElement('div');
markdown.className = 'markdown-content';
const pre = document.createElement('pre');
pre.className = 'markdown-content__pre cr-code-block__pre';
const code = document.createElement('code');
code.dataset.fontCategory = item.category;
code.className = 'language-typescript';
code.textContent = item.text;
pre.append(code);
markdown.append(pre);
frame.append(markdown);
}
root.append(message);
}
(document.querySelector('.app-shell') || document.body).append(root);
return { count: root.querySelectorAll('[data-font-category]').length, text: samples.map((item) => item.text) };
})()`
}
export async function captureYcodePlatformFonts(samples) {
const port = await reservePort()
const userDataDir = path.join(projectRoot, '.tmp', `rea-font-probe-profile-${process.pid}`)
await mkdir(userDataDir, { recursive: true })
const electronPath = process.env.YCODE_ELECTRON_PATH || require('electron')
const child = spawn(electronPath, [
`--remote-debugging-port=${port}`,
`--user-data-dir=${userDataDir}`,
'--no-first-run',
projectRoot
], { cwd: projectRoot, stdio: 'ignore', windowsHide: true })
let cdp
try {
const browserInfo = await waitForDevtools(port, child)
const target = await waitForPageTarget(port, child)
cdp = connectCdp(target.webSocketDebuggerUrl)
await cdp.opened
await cdp.send('Page.enable')
await cdp.send('Runtime.enable')
await cdp.send('DOM.enable')
await cdp.send('CSS.enable')
const ready = await cdp.send('Runtime.evaluate', {
expression: `new Promise((resolve) => { const start = Date.now(); const tick = () => { if (document.querySelector('.app-shell')) resolve(true); else if (Date.now() - start > 25000) resolve(false); else setTimeout(tick, 100); }; tick(); })`,
awaitPromise: true,
returnByValue: true
})
if (ready.result?.value !== true) fail('YCode renderer did not reach its app shell')
await cdp.send('Runtime.evaluate', { expression: 'document.fonts.ready.then(() => true)', awaitPromise: true, returnByValue: true })
const injected = await cdp.send('Runtime.evaluate', { expression: buildProbeExpression(samples), returnByValue: true })
if (injected.exceptionDetails) fail(`Could not inject font probes: ${injected.exceptionDetails.text}`)
await cdp.send('Runtime.evaluate', { expression: 'document.fonts.ready.then(() => true)', awaitPromise: true, returnByValue: true })
await cdp.send('Runtime.evaluate', {
expression: 'new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))',
awaitPromise: true,
returnByValue: true
})
const document = await cdp.send('DOM.getDocument', { depth: -1, pierce: true })
const capturedSamples = []
for (const sample of samples) {
const selector = `#yc-font-platform-probe [data-font-category="${sample.category}"]`
const { nodeId } = await cdp.send('DOM.querySelector', { nodeId: document.root.nodeId, selector })
if (!nodeId) fail(`YCode probe node not found for ${sample.category}`)
const details = await cdp.send('Runtime.evaluate', {
expression: `(() => { const node = document.querySelector(${JSON.stringify(selector)}); const style = getComputedStyle(node); const rect = node.getBoundingClientRect(); return { text: node.textContent, fontFamily: style.fontFamily, fontSize: style.fontSize, fontWeight: style.fontWeight, width: rect.width, height: rect.height }; })()`,
returnByValue: true
})
if (details.exceptionDetails) fail(`Could not read YCode ${sample.category} computed style`)
const platform = await cdp.send('CSS.getPlatformFontsForNode', { nodeId })
const value = details.result?.value
if (!value || value.text !== sample.text) fail(`YCode ${sample.category} probe text differs from the REA sample`)
capturedSamples.push({
category: sample.category,
text: value.text,
fontFamily: value.fontFamily,
fontSize: value.fontSize,
fontWeight: value.fontWeight,
...(sample.category === 'code' ? { codeContext: sample.codeContext ?? 'fenced' } : {}),
platformFonts: normalizeFonts(platform.fonts, `YCode ${sample.category}`)
})
}
const env = {
platform: process.platform,
osRelease: os.release(),
osVersion: os.version(),
arch: process.arch,
electron: require('electron/package.json').version,
chromium: browserInfo.Browser ?? null,
userAgent: browserInfo['User-Agent'] ?? null
}
return { environment: env, samples: capturedSamples }
} finally {
if (cdp) {
try { await cdp.send('Runtime.evaluate', { expression: 'window.close()' }) } catch { /* The renderer may already be gone. */ }
}
cdp?.close()
if (child.exitCode === null) {
await Promise.race([
new Promise((resolve) => child.once('exit', resolve)),
delay(3000)
])
}
if (child.exitCode === null) child.kill()
await rm(userDataDir, { recursive: true, force: true })
}
}
async function main() {
const referenceBytes = await readFile(referencePath).catch(() => null)
if (!referenceBytes) fail(`REA platform-font report not found: ${referencePath}`)
const referenceReport = JSON.parse(referenceBytes.toString('utf8'))
const reference = await validateReferenceReport(referenceReport, referencePath)
const reportSha256 = createHash('sha256').update(referenceBytes).digest('hex')
const local = await captureYcodePlatformFonts(reference.samples)
const localSamples = local.samples
const comparisons = reference.samples.map((expected, index) => {
const actual = localSamples[index]
const sameFonts = JSON.stringify(expected.platformFonts) === JSON.stringify(actual.platformFonts)
const referenceSize = expected.fontSize ? Number.parseFloat(expected.fontSize) : null
const localSize = actual.fontSize ? Number.parseFloat(actual.fontSize) : null
const fontSizeMatches = referenceSize === null || (localSize !== null && Math.abs(referenceSize - localSize) <= 0.01)
const fontWeightMatches = expected.fontWeight === null || expected.fontWeight === actual.fontWeight
const mismatchReasons = []
if (expected.text !== actual.text) mismatchReasons.push('sample text differs')
if (!sameFonts) mismatchReasons.push('actual platform font records differ')
if (!fontSizeMatches) mismatchReasons.push(`font size differs (${expected.fontSize} vs ${actual.fontSize})`)
if (!fontWeightMatches) mismatchReasons.push(`font weight differs (${expected.fontWeight} vs ${actual.fontWeight})`)
if (expected.fontFamily && expected.fontFamily !== actual.fontFamily) mismatchReasons.push('computed CSS font-family stack differs')
return {
category: expected.category,
status: expected.text === actual.text && sameFonts && fontSizeMatches && fontWeightMatches ? 'matched' : 'mismatched',
mismatchReasons,
textMatches: expected.text === actual.text,
cssFontFamilyMatches: expected.fontFamily ? expected.fontFamily === actual.fontFamily : null,
fontSizeMatches,
referenceFontSize: expected.fontSize,
ycodeFontSize: actual.fontSize,
fontWeightMatches,
referenceFontWeight: expected.fontWeight,
ycodeFontWeight: actual.fontWeight,
...(expected.category === 'code' ? { codeContext: expected.codeContext } : {}),
referenceFontFamily: expected.fontFamily,
ycodeFontFamily: actual.fontFamily,
referencePlatformFonts: expected.platformFonts,
ycodePlatformFonts: actual.platformFonts,
platformFontsMatch: sameFonts
}
})
comparisons.push(...reference.missing.map((entry) => ({
category: entry.category,
status: 'missing',
reason: entry.reason,
platformFontsMatch: null
})))
const referenceEnvironment = reference.environment
const environmentMatches = !referenceEnvironment || (
(!referenceEnvironment.platform || referenceEnvironment.platform === local.environment.platform) &&
(!referenceEnvironment.osRelease || referenceEnvironment.osRelease === local.environment.osRelease)
)
const samplesComplete = reference.missing.length === 0 && reference.samples.length === requiredCategories.length
const complete = samplesComplete && reference.versionObserved
const passed = complete && environmentMatches && comparisons.every((item) => item.status === 'matched')
const status = !samplesComplete || !reference.versionObserved
? 'incomplete'
: reference.kind === 'ycode-electron-self-test'
? passed ? 'self-test-passed' : 'self-test-failed'
: passed ? 'passed' : 'failed'
const result = {
schemaVersion: 1,
reference: {
kind: reference.kind,
acceptanceScope: reference.kind === 'rea-workbuddy' ? 'workbuddy-parity' : 'local-electron-self-test-only',
originalParity: reference.kind === 'rea-workbuddy' && status === 'passed',
path: path.relative(projectRoot, referencePath).replaceAll('\\', '/'),
reportSha256,
evidenceId: reference.evidenceId,
version: reference.version,
versionObserved: reference.versionObserved,
archive: reference.archive,
evidenceSha256: reference.archiveSha256,
archiveVerified: reference.archiveVerified,
environment: referenceEnvironment
},
ycode: { environment: local.environment, samples: localSamples },
environmentMatches,
samplesComplete,
complete,
comparisons,
status,
passed
}
if (!reference.versionObserved) result.incompleteReasons = ['WorkBuddy version is unobserved in the archived REA Evidence']
else if (!samplesComplete) result.incompleteReasons = reference.missing.map((entry) => `${entry.category}: ${entry.reason}`)
await mkdir(path.dirname(resultPath), { recursive: true })
await writeFile(resultPath, `${JSON.stringify(result, null, 2)}\n`)
console.log(JSON.stringify({
status,
passed,
complete,
reference: {
kind: reference.kind,
acceptanceScope: result.reference.acceptanceScope,
originalParity: result.reference.originalParity,
evidenceId: reference.evidenceId,
version: reference.version,
reportSha256,
evidenceSha256: reference.archiveSha256,
archiveVerified: reference.archiveVerified
},
environmentMatches,
samplesComplete,
samples: comparisons.map(({ category, status: sampleStatus, reason, mismatchReasons, codeContext, cssFontFamilyMatches, fontSizeMatches, referenceFontSize, ycodeFontSize, fontWeightMatches, referenceFontWeight, ycodeFontWeight, platformFontsMatch, referencePlatformFonts, ycodePlatformFonts }) => ({ category, status: sampleStatus, reason, mismatchReasons, codeContext, cssFontFamilyMatches, fontSizeMatches, referenceFontSize, ycodeFontSize, fontWeightMatches, referenceFontWeight, ycodeFontWeight, platformFontsMatch, referencePlatformFonts, ycodePlatformFonts })),
incompleteReasons: result.incompleteReasons,
resultPath: path.relative(projectRoot, resultPath).replaceAll('\\', '/')
}, null, 2))
if (!passed) process.exitCode = complete ? 1 : 2
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error))
process.exitCode = 1
})
}