import { readdir, readFile, stat, writeFile } from 'node:fs/promises' import { join, relative, resolve } from 'node:path' const references = [ { viewport: 'desktop', name: 'weekend-between-races' }, { viewport: 'desktop', name: 'weekend-live' }, { viewport: 'mobile', name: 'weekend-between-sessions-mobile' }, ] function option(name, fallback) { const index = process.argv.indexOf(`--${name}`) return index === -1 ? fallback : process.argv[index + 1] } async function filesBelow(directory) { const entries = await readdir(directory, { withFileTypes: true }) const files = await Promise.all(entries.map(async (entry) => { const path = join(directory, entry.name) return entry.isDirectory() ? filesBelow(path) : [path] })) return files.flat() } async function locateMockup(root, name) { const preferred = join(root, `${name}.png`) try { await stat(preferred) return preferred } catch { const matches = (await filesBelow(root)).filter((path) => path.endsWith(`${name}.png`)) return matches.length === 1 ? matches[0] : null } } function imageData(path) { return readFile(path).then((data) => `data:image/png;base64,${data.toString('base64')}`) } export async function generatePacket({ version, evidence, mockups }) { const candidateRoot = join(evidence, 'candidate') const pairs = [] const missing = [] try { await stat(mockups) } catch { throw new Error(`Approved mockups directory is missing: ${mockups}. It is supplied by the product reference integration.`) } for (const { viewport, name } of references) { const candidate = join(candidateRoot, viewport, `${name}.png`) const reference = await locateMockup(mockups, name) try { await stat(candidate) } catch { missing.push(`candidate: ${candidate}`) } if (!reference) missing.push(`mockup: ${join(mockups, `${name}.png`)}`) if (reference) pairs.push({ viewport, name, candidate, reference }) } if (missing.length) { throw new Error(`Release-fidelity packet is incomplete:\n${missing.join('\n')}`) } const cards = await Promise.all(pairs.map(async ({ viewport, name, candidate, reference }) => `

${viewport}: ${name}

Approved mockup
Candidate

Reference: ${relative(process.cwd(), reference)}

`)) const summary = `# Release Fidelity Evidence: ${version}\n\n- Candidate screenshots: candidate/\n- Approved mockups: ${relative(process.cwd(), mockups)}\n- Review: human owner decision required; this packet does not approve the release.\n\nOpen index.html for side-by-side evidence.\n` const html = `${version} release-fidelity review

${version} Release-Fidelity Review

Visual regression is a separate automated gate. This packet is evidence for a subjective owner review and is not approval.

${cards.join('')}` await writeFile(join(evidence, 'summary.md'), summary) await writeFile(join(evidence, 'index.html'), html) } if (import.meta.url === `file://${process.argv[1]}`) { const version = option('version', process.env.RELEASE_FIDELITY_VERSION ?? 'v0.4.0') const output = process.env.RELEASE_FIDELITY_OUTPUT ?? 'release-fidelity' const evidence = resolve(option('evidence', join(output, version))) const mockups = resolve(option('mockups', join('docs/product', version, 'mockups'))) generatePacket({ version, evidence, mockups }).then( () => console.log(`Release-fidelity packet: ${join(evidence, 'index.html')}`), (error) => { console.error(error.message); process.exitCode = 1 }, ) }