#!/usr/bin/env node import { spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const skillRoot = path.resolve(__dirname, '..'); const TYPES = new Set(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']); function usage() { return `Usage: archify render [output.html] [--quality standard|showcase] [--repo-root path (architecture only)] archify compare architecture [output.html] [--receipt path] [--json] [--quality standard|showcase] [--repo-root path] archify deliver [output.html] [--json] [--open] [--quality standard|showcase] [--repo-root path (architecture only)] archify preview [output.html] [--no-open] [--quality standard|showcase] [--repo-root path (architecture only)] archify validate [--json] [--layout-json] [--quality standard|showcase] [--repo-root path (architecture only)] archify migrate workflow --to-schema 2 [--json] archify inspect archify check archify visual-check [--json] archify guide [scenario or question] [--json] [--lang en|zh] archify brands [name, alias, domain, or category] [--json] archify brands capture [--json] archify examples archify doctor archify demo [output-directory] Types: architecture, workflow, sequence, dataflow, lifecycle `; } function fail(message, code = 2) { console.error(message); process.exit(code); } function rendererPath(type) { if (!TYPES.has(type)) { fail(`Unknown diagram type "${type}". Expected one of: ${[...TYPES].join(', ')}`); } return path.join(skillRoot, 'renderers', type, `render-${type}.mjs`); } function runNode(args, options = {}) { return spawnSync(process.execPath, args, { cwd: options.cwd || process.cwd(), encoding: 'utf8', stdio: options.stdio || 'inherit', env: options.env ? { ...process.env, ...options.env } : process.env, }); } function extractQualityArgs(args) { const rest = []; let quality; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === '--quality') { quality = args[index + 1]; if (!quality || quality.startsWith('--')) fail('--quality requires standard or showcase.'); index += 1; continue; } if (arg.startsWith('--quality=')) { quality = arg.slice('--quality='.length); if (!quality) fail('--quality requires standard or showcase.'); continue; } rest.push(arg); } if (quality !== undefined && !['standard', 'showcase'].includes(quality)) { fail(`Unknown quality profile "${quality}". Expected standard or showcase.`); } return { rest, quality }; } function extractRepoRootArgs(args) { const rest = []; let repoRoot; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === '--repo-root') { repoRoot = args[index + 1]; if (!repoRoot || repoRoot.startsWith('--')) fail('--repo-root requires a repository path.'); index += 1; continue; } if (arg.startsWith('--repo-root=')) { repoRoot = arg.slice('--repo-root='.length); if (!repoRoot) fail('--repo-root requires a repository path.'); continue; } rest.push(arg); } return { rest, repoRoot: repoRoot ? path.resolve(repoRoot) : undefined }; } function rendererEnv(quality, repoRoot, diagnosticJson = false) { return { ...(quality ? { ARCHIFY_QUALITY_PROFILE: quality } : {}), ...(repoRoot ? { ARCHIFY_REPO_ROOT: repoRoot } : {}), ...(diagnosticJson ? { ARCHIFY_DIAGNOSTIC_FORMAT: 'json' } : {}), }; } function diagnostic({ code, message, subject = {}, evidence = {}, supportedFixes = [], severity = 'error' }) { return { code, severity, message, subject, evidence, supportedFixes, }; } function inputDiagnostic(error, inputPath) { const isSyntax = error instanceof SyntaxError; return diagnostic({ code: isSyntax ? 'input/json-parse' : 'input/read', message: isSyntax ? `Input JSON could not be parsed: ${error.message}` : `Input could not be read: ${error.message}`, subject: { input: inputPath }, evidence: { ...(error?.code ? { systemCode: error.code } : {}), reason: error.message, }, supportedFixes: [isSyntax ? 'repair the JSON syntax and run validation again' : 'provide one readable JSON input file'], }); } function rendererFailure(result) { if (result.error) { return { error: 'Renderer process could not start.', diagnostics: [diagnostic({ code: 'internal/renderer-process', message: 'Renderer process could not start.', evidence: { reason: result.error.message }, })], }; } try { const payload = JSON.parse((result.stderr || '').trim()); if (payload?.ok === false && Array.isArray(payload.diagnostics) && payload.diagnostics.length) { return { error: payload.error || payload.diagnostics[0].message, diagnostics: payload.diagnostics, }; } } catch { // The diagnostic boundary is intentionally fail-closed. Never copy a raw // Node stack into a machine receipt when a renderer exits unexpectedly. } return { error: 'Renderer failed before emitting a structured diagnostic.', diagnostics: [diagnostic({ code: 'internal/unclassified', message: 'Renderer failed before emitting a structured diagnostic.', evidence: { exitCode: result.status ?? 1 }, })], }; } const COMPOSITION_CHECKS = new Set([ 'label_route_clearance', 'relationship_crossings', 'relationship_corridors', 'container_border_runs', 'route_rhythm', ]); const CHECK_FIXES = { single_svg: ['remove additional SVG roots so the artifact contains exactly one diagram SVG'], finite_svg: ['replace non-finite coordinates before rendering again'], orthogonal_arrows: ['use renderer-supported orthogonal routing controls'], legend_clearance: ['move the route or enlarge the viewBox so relationships do not enter the legend'], }; const COMPOSITION_FIXES = { 'composition/proper-crossing': ['adjust route/via or channel coordinates so unrelated relationships use separate corridors'], 'composition/ambiguous-corridor': ['adjust route/via or channel coordinates so unrelated relationships do not visually merge'], 'composition/container-border-run': ['route across the frame perpendicularly through a clear opening'], 'composition/label-route-clearance': ['adjust labelAt, labelDx, labelDy, labelSegment, message y, or the other relationship route'], 'composition/desktop-readability': ['reduce the viewBox width, shorten node copy, widen affected nodes, or split the diagram so node context remains at least 6px at a 1440px desktop viewport'], 'composition/micro-segment': ['move the route/channel/via point so every visible segment is at least 8px'], 'composition/short-interior-segment': ['move the route/channel/via point so every interior turn has at least 16px'], }; function checkerDiagnostics(checker) { const diagnostics = []; for (const issue of checker?.composition?.issues || []) { if (issue.severity !== 'error') continue; const { severity, code, relationship, ...evidence } = issue; diagnostics.push(diagnostic({ code, severity, message: `Final artifact failed ${code}.`, subject: relationship ? { relationship } : { check: 'composition' }, evidence, supportedFixes: COMPOSITION_FIXES[code] || [], })); } for (const check of checker?.checks || []) { if (check.ok || COMPOSITION_CHECKS.has(check.name)) continue; diagnostics.push(diagnostic({ code: `artifact/${check.name.replaceAll('_', '-')}`, message: (check.details || []).find(Boolean) || `Final artifact failed ${check.name}.`, subject: { check: check.name }, evidence: { details: check.details || [] }, supportedFixes: CHECK_FIXES[check.name] || [], })); } return diagnostics.length ? diagnostics : [diagnostic({ code: 'artifact/check-failed', message: 'Final artifact check failed without a classified diagnostic.', subject: { check: 'unknown' }, evidence: {}, })]; } function formatDiagnostics(error, diagnostics = []) { if (!diagnostics.length) return error; return [ error, ...diagnostics.map((entry) => { const fix = entry.supportedFixes?.length ? ` Fix: ${entry.supportedFixes.join('; ')}.` : ''; return `[${entry.code}] ${entry.message}${fix}`; }), ].join('\n'); } function assertEvidenceType(type, repoRoot) { if (repoRoot && type !== 'architecture') { fail('--repo-root is currently supported for architecture diagrams only.'); } } function exitFrom(result) { if (result.error) fail(result.error.message, 1); process.exit(result.status ?? 1); } function reportCompareFailure({ json, stage, error, code = 'delta/internal', details = {}, status = 1 }) { const receipt = { schemaVersion: 1, ok: false, command: 'compare', type: 'architecture', stage, error, diagnostics: [{ code, severity: 'error', message: error, subject: details.side ? { side: details.side, ...(details.path ? { path: details.path } : {}) } : {}, evidence: Object.fromEntries(Object.entries(details).filter(([key]) => !['side', 'path', 'supportedFixes'].includes(key))), supportedFixes: details.supportedFixes || [], }], }; if (json) console.log(JSON.stringify(receipt, null, 2)); else console.error(formatDiagnostics(error, receipt.diagnostics)); process.exitCode = status; } function extractCompareOptions(args) { const positional = []; let receipt; let json = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === '--json') { json = true; continue; } if (arg === '--receipt') { receipt = args[index + 1]; if (!receipt || receipt.startsWith('--')) fail('--receipt requires a JSON output path.'); index += 1; continue; } if (arg.startsWith('--receipt=')) { receipt = arg.slice('--receipt='.length); if (!receipt) fail('--receipt requires a JSON output path.'); continue; } if (arg.startsWith('--')) fail(`Unknown compare option "${arg}".`); positional.push(arg); } return { positional, receipt, json }; } function compareReceiptPath(outputPath) { const extension = path.extname(outputPath); return extension ? `${outputPath.slice(0, -extension.length)}.receipt.json` : `${outputPath}.receipt.json`; } function compareCommitError(message, code, details = {}) { const error = new Error(message); error.compareStage = 'commit'; error.compareCode = code; error.compareDetails = details; return error; } function commitComparePair({ htmlCandidate, receiptCandidate, outputPath, receiptPath, stagingDirectory }) { const targets = [ { label: 'HTML artifact', target: outputPath, candidate: htmlCandidate, backup: path.join(stagingDirectory, '.previous-output') }, { label: 'receipt', target: receiptPath, candidate: receiptCandidate, backup: path.join(stagingDirectory, '.previous-receipt') }, ]; // Preflight the whole pair before moving either trusted target. This avoids // replacing the HTML and only then discovering that its receipt destination // cannot be committed (for example, because it is a directory). for (const item of targets) { if (!fs.existsSync(item.target)) continue; const existing = fs.lstatSync(item.target); if (!existing.isFile()) { throw compareCommitError( `Could not commit Architecture Delta: existing ${item.label} target is not a regular file.`, 'delta/commit-target', { target: path.basename(item.target), targetType: existing.isDirectory() ? 'directory' : 'non-file', supportedFixes: [`choose a regular-file path for the ${item.label}`], }, ); } } const backedUp = []; const committed = []; try { for (const item of targets) { if (!fs.existsSync(item.target)) continue; fs.renameSync(item.target, item.backup); backedUp.push(item); } for (const item of targets) { fs.renameSync(item.candidate, item.target); committed.push(item); } } catch (cause) { const rollbackErrors = []; for (const item of [...committed].reverse()) { try { fs.rmSync(item.target, { force: true }); } catch (error) { rollbackErrors.push(`${item.label}: remove failed (${error.message})`); } } for (const item of [...backedUp].reverse()) { try { if (fs.existsSync(item.target)) fs.rmSync(item.target, { force: true }); fs.renameSync(item.backup, item.target); } catch (error) { rollbackErrors.push(`${item.label}: restore failed (${error.message})`); } } throw compareCommitError( rollbackErrors.length ? 'Architecture Delta pair commit failed and its previous files could not be fully restored.' : 'Architecture Delta pair commit failed; the previous files were restored.', rollbackErrors.length ? 'delta/commit-rollback-failed' : 'delta/commit-failed', { reason: cause.message, ...(rollbackErrors.length ? { rollbackErrors } : {}), supportedFixes: ['check that both output paths are writable regular files, then retry'], }, ); } } function renderValidatedArchitecture(inputPath, outputPath, quality, repoRoot) { const render = runNode([rendererPath('architecture'), inputPath, outputPath], { stdio: 'pipe', env: rendererEnv(quality, repoRoot, true), }); if (render.status !== 0) { const failure = rendererFailure(render); const error = new Error(failure.error); error.compareStage = 'input'; error.compareStatus = render.status ?? 1; error.diagnostics = failure.diagnostics; throw error; } const check = runNode([path.join(skillRoot, 'scripts/check-render-output.mjs'), outputPath], { stdio: 'pipe' }); if (check.status !== 0) { const error = new Error('Validated snapshot failed final artifact checks.'); error.compareStage = 'check'; error.compareStatus = check.status ?? 1; try { error.checker = JSON.parse(check.stdout); error.diagnostics = checkerDiagnostics(error.checker); } catch { error.diagnostics = []; } throw error; } const artifact = fs.readFileSync(outputPath); return { artifact, html: artifact.toString('utf8'), checks: JSON.parse(check.stdout), sourceEvidence: sourceEvidenceFromArtifact(artifact), }; } async function commandCompare(args) { const { resolveOutputPath } = await import('../renderers/shared/output-path.mjs'); const qualityArgs = extractQualityArgs(args); const repoArgs = extractRepoRootArgs(qualityArgs.rest); const options = extractCompareOptions(repoArgs.rest); const [type, baseInput, headInput, requestedOutput] = options.positional; if (type !== 'architecture' || !baseInput || !headInput || options.positional.length > 4) fail(usage()); let deltaRuntime; try { deltaRuntime = await import(pathToFileURL(path.join(skillRoot, 'delta/architecture-delta.mjs')).href); } catch (error) { reportCompareFailure({ json: options.json, stage: 'prepare', error: 'Architecture compare runtime is unavailable.', code: 'delta/runtime-missing', details: { reason: error.message, supportedFixes: ['install the complete Archify skill package'] } }); return; } const { ArchitectureDeltaError, annotateArchitectureSideSvg, buildDeltaSvg, canonicalArchitecture, canonicalArchitectureJson, compareArchitecture, extractArchitectureSvg, extractArtifactCss, renderArchitectureDeltaHtml, validateArchitectureDeltaHtml, } = deltaRuntime; const basePath = path.resolve(baseInput); const headPath = path.resolve(headInput); let outputPath; try { ({ outputPath } = resolveOutputPath({ requestedOutput, defaultOutput: 'architecture-delta.html', inputPaths: [basePath, headPath], })); } catch (error) { const outputDiagnostic = error.archifyDiagnostics?.[0]; reportCompareFailure({ json: options.json, stage: 'prepare', error: error.message, code: outputDiagnostic?.code || 'output/path-resolution', details: { ...(outputDiagnostic?.subject || {}), ...(outputDiagnostic?.evidence || {}), supportedFixes: outputDiagnostic?.supportedFixes || ['choose a safe output path and retry'], }, }); return; } let receiptPath; try { ({ outputPath: receiptPath } = resolveOutputPath({ requestedOutput: options.receipt || compareReceiptPath(outputPath), defaultOutput: compareReceiptPath(outputPath), inputPaths: [basePath, headPath], otherOutputPaths: [outputPath], })); } catch (error) { const outputDiagnostic = error.archifyDiagnostics?.[0]; reportCompareFailure({ json: options.json, stage: 'prepare', error: error.message, code: outputDiagnostic?.code || 'output/path-resolution', details: { ...(outputDiagnostic?.subject || {}), ...(outputDiagnostic?.evidence || {}), supportedFixes: outputDiagnostic?.supportedFixes || ['choose a safe receipt path and retry'], }, }); return; } let baseBuffer; let headBuffer; let base; let head; try { baseBuffer = fs.readFileSync(basePath); base = JSON.parse(baseBuffer.toString('utf8')); } catch (error) { reportCompareFailure({ json: options.json, stage: 'input', error: `Could not read base input: ${error.message}`, code: 'delta/base-input', details: { side: 'base', reason: error.message } }); return; } try { headBuffer = fs.readFileSync(headPath); head = JSON.parse(headBuffer.toString('utf8')); } catch (error) { reportCompareFailure({ json: options.json, stage: 'input', error: `Could not read head input: ${error.message}`, code: 'delta/head-input', details: { side: 'head', reason: error.message } }); return; } const outputDirectory = path.dirname(outputPath); if (path.dirname(receiptPath) !== outputDirectory) { reportCompareFailure({ json: options.json, stage: 'prepare', error: 'The compare receipt must be written beside the HTML artifact.', code: 'delta/receipt-directory', details: { supportedFixes: ['choose a --receipt path in the same directory as output.html'] } }); return; } try { fs.mkdirSync(outputDirectory, { recursive: true }); } catch (error) { reportCompareFailure({ json: options.json, stage: 'prepare', error: `Could not create compare output directory: ${error.message}`, code: 'delta/output-directory', details: { reason: error.message } }); return; } let stagingDirectory; try { stagingDirectory = fs.mkdtempSync(path.join(outputDirectory, '.archify-compare-')); } catch (error) { reportCompareFailure({ json: options.json, stage: 'prepare', error: `Could not create compare candidate: ${error.message}`, code: 'delta/candidate-directory', details: { reason: error.message } }); return; } const baseCandidate = path.join(stagingDirectory, 'base.html'); const headCandidate = path.join(stagingDirectory, 'head.html'); const rawBaseCandidate = path.join(stagingDirectory, 'base.raw.html'); const rawHeadCandidate = path.join(stagingDirectory, 'head.raw.html'); const canonicalBaseInput = path.join(stagingDirectory, 'base.architecture.json'); const canonicalHeadInput = path.join(stagingDirectory, 'head.architecture.json'); const htmlCandidate = path.join(stagingDirectory, path.basename(outputPath)); const receiptCandidate = path.join(stagingDirectory, path.basename(receiptPath)); try { let baseResult; let headResult; try { renderValidatedArchitecture(basePath, rawBaseCandidate, qualityArgs.quality, repoArgs.repoRoot); } catch (error) { const diagnosticEntry = error.diagnostics?.[0]; reportCompareFailure({ json: options.json, stage: error.compareStage || 'validate', error: `Base snapshot failed validation: ${error.message}`, code: diagnosticEntry?.code || 'delta/base-validation', details: { side: 'base', ...(diagnosticEntry?.subject?.path ? { path: diagnosticEntry.subject.path } : {}), ...(diagnosticEntry?.evidence || {}), supportedFixes: diagnosticEntry?.supportedFixes || [] }, status: error.compareStatus || 1, }); return; } try { renderValidatedArchitecture(headPath, rawHeadCandidate, qualityArgs.quality, repoArgs.repoRoot); } catch (error) { const diagnosticEntry = error.diagnostics?.[0]; reportCompareFailure({ json: options.json, stage: error.compareStage || 'validate', error: `Head snapshot failed validation: ${error.message}`, code: diagnosticEntry?.code || 'delta/head-validation', details: { side: 'head', ...(diagnosticEntry?.subject?.path ? { path: diagnosticEntry.subject.path } : {}), ...(diagnosticEntry?.evidence || {}), supportedFixes: diagnosticEntry?.supportedFixes || [] }, status: error.compareStatus || 1, }); return; } // Validation must see the exact authored inputs. Only after both sides // pass do we canonicalize their collection order for deterministic SVG // geometry and stable artifact bytes. fs.writeFileSync(canonicalBaseInput, JSON.stringify(canonicalArchitecture(base))); fs.writeFileSync(canonicalHeadInput, JSON.stringify(canonicalArchitecture(head))); baseResult = renderValidatedArchitecture(canonicalBaseInput, baseCandidate, qualityArgs.quality, repoArgs.repoRoot); headResult = renderValidatedArchitecture(canonicalHeadInput, headCandidate, qualityArgs.quality, repoArgs.repoRoot); const semanticHash = (diagram) => createHash('sha256').update(canonicalArchitectureJson(diagram)).digest('hex'); let compareIr; try { compareIr = compareArchitecture(base, head, { baseRawSha256: createHash('sha256').update(baseBuffer).digest('hex'), headRawSha256: createHash('sha256').update(headBuffer).digest('hex'), baseSemanticSha256: semanticHash(base), headSemanticSha256: semanticHash(head), baseBytes: baseBuffer.byteLength, headBytes: headBuffer.byteLength, baseVerified: Boolean(baseResult.sourceEvidence), headVerified: Boolean(headResult.sourceEvidence), }); } catch (error) { if (!(error instanceof ArchitectureDeltaError)) throw error; reportCompareFailure({ json: options.json, stage: 'compare', error: error.message, code: error.code, details: error.details }); return; } const baseSourceSvg = extractArchitectureSvg(baseResult.html); const headSourceSvg = extractArchitectureSvg(headResult.html); const baseSvg = annotateArchitectureSideSvg(baseSourceSvg, compareIr, 'base'); const headSvg = annotateArchitectureSideSvg(headSourceSvg, compareIr, 'head'); const deltaSvg = buildDeltaSvg(baseSourceSvg, headSourceSvg, compareIr); // Raw input hashes and byte counts belong in the sidecar receipt, not the // artifact. Keeping them out makes formatting-only input rewrites produce // the exact same canonical review HTML and artifact hash. const artifactIr = { ...compareIr, base: Object.fromEntries(Object.entries(compareIr.base).filter(([key]) => !['rawSha256', 'bytes'].includes(key))), head: Object.fromEntries(Object.entries(compareIr.head).filter(([key]) => !['rawSha256', 'bytes'].includes(key))), }; const html = renderArchitectureDeltaHtml({ receipt: artifactIr, baseSvg, deltaSvg, headSvg, baseHtml: baseResult.html, headHtml: headResult.html, artifactCss: extractArtifactCss(headResult.html), }); const deltaValidation = validateArchitectureDeltaHtml(html, artifactIr); fs.writeFileSync(htmlCandidate, html); const artifact = fs.readFileSync(htmlCandidate); const baseChecks = baseResult.checks.checks.filter((check) => check.ok).length; const headChecks = headResult.checks.checks.filter((check) => check.ok).length; const finalReceipt = { ...compareIr, artifact: { sha256: createHash('sha256').update(artifact).digest('hex'), bytes: artifact.byteLength }, validation: { checksPassed: baseChecks + headChecks + deltaValidation.checksPassed, checkCount: baseResult.checks.checks.length + headResult.checks.checks.length + deltaValidation.checkCount, baseComposition: baseResult.checks.composition.status, headComposition: headResult.checks.composition.status, }, }; fs.writeFileSync(receiptCandidate, `${JSON.stringify(finalReceipt, null, 2)}\n`); try { const currentOutput = resolveOutputPath({ requestedOutput, defaultOutput: 'architecture-delta.html', inputPaths: [basePath, headPath], }).outputPath; resolveOutputPath({ requestedOutput: options.receipt || compareReceiptPath(currentOutput), defaultOutput: compareReceiptPath(currentOutput), inputPaths: [basePath, headPath], otherOutputPaths: [currentOutput], }); } catch (error) { const outputDiagnostic = error.archifyDiagnostics?.[0]; reportCompareFailure({ json: options.json, stage: 'commit', error: error.message, code: outputDiagnostic?.code || 'output/path-resolution', details: { ...(outputDiagnostic?.subject || {}), ...(outputDiagnostic?.evidence || {}), supportedFixes: outputDiagnostic?.supportedFixes || ['restore safe output paths and retry'], }, }); return; } commitComparePair({ htmlCandidate, receiptCandidate, outputPath, receiptPath, stagingDirectory }); if (options.json) console.log(JSON.stringify(finalReceipt, null, 2)); else { console.log(`compared architecture ${outputPath}`); console.log(`${finalReceipt.validation.checksPassed}/${finalReceipt.validation.checkCount} checks; completeness ${finalReceipt.completeness}; ${finalReceipt.proofLevel}; sha256 ${finalReceipt.artifact.sha256.slice(0, 12)}`); console.log(`receipt ${receiptPath}`); } } catch (error) { if (error instanceof ArchitectureDeltaError) { reportCompareFailure({ json: options.json, stage: 'artifact', error: error.message, code: error.code, details: error.details }); } else if (error.compareStage === 'commit') { reportCompareFailure({ json: options.json, stage: error.compareStage, error: error.message, code: error.compareCode, details: error.compareDetails, }); } else { reportCompareFailure({ json: options.json, stage: 'internal', error: 'Architecture compare failed before commit.', code: 'delta/internal', details: { reason: error.message } }); } } finally { try { fs.rmSync(stagingDirectory, { recursive: true, force: true }); } catch (error) { console.error(`Warning: could not remove compare staging directory: ${error.message}`); } } } function commandRender(args) { const qualityArgs = extractQualityArgs(args); const repoArgs = extractRepoRootArgs(qualityArgs.rest); const [type, input, output] = repoArgs.rest; if (!type || !input) fail(usage()); assertEvidenceType(type, repoArgs.repoRoot); const result = runNode([rendererPath(type), input, ...(output ? [output] : [])], { env: rendererEnv(qualityArgs.quality, repoArgs.repoRoot), }); if (result.status !== 0) exitFrom(result); } function reportArtifactFailure({ command, json, stage, type, input, output, error, diagnostics = [], status = 1, checker }) { const receipt = { schemaVersion: 1, ok: false, command, stage, type, input, ...(output === undefined ? {} : { output }), error, diagnostics, ...(checker ? { checker } : {}), }; if (json) console.log(JSON.stringify(receipt, null, 2)); else console.error(formatDiagnostics(error, diagnostics)); process.exitCode = status; } function reportDeliveryFailure(options) { reportArtifactFailure({ ...options, command: 'deliver' }); } function reportValidateFailure(options) { reportArtifactFailure({ ...options, command: 'validate' }); } function sourceEvidenceFromArtifact(artifact) { const html = artifact.toString('utf8'); const match = html.match(/