Archify Diagram Viewer 0.1.0

VS Code extension that previews Archify diagrams from their JSON sources
(live, as you type) and opens rendered Archify HTML in a viewer tab.
Bundles the Archify 3.0.1 renderer and runs it on VS Code's Node runtime.
Adds validation diagnostics, JSON schema help, source-link navigation,
export saving, render-to-file and open-in-browser commands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 12:40:55 +03:00
co-authored by Claude Opus 5.5
commit d56e784e97
119 changed files with 66708 additions and 0 deletions
+6936
View File
File diff suppressed because it is too large Load Diff
+91
View File
@@ -0,0 +1,91 @@
import { spawn } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const childPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'delivery-update-child.mjs');
const DEADLINE_MS = 1_000;
const MAX_OUTPUT_BYTES = 16 * 1_024;
function unavailable(reason) {
return { status: 'unavailable', installedVersion: null, availableVersion: null,
releaseNotes: null, checkedAt: null, source: null, noticeRequired: false,
noticeText: null, reason };
}
function normalize(result) {
if (result?.status === 'current') return {
status: 'current', installedVersion: result.installedVersion,
availableVersion: result.availableVersion, releaseNotes: null,
checkedAt: result.checkedAt, source: result.source,
noticeRequired: false, noticeText: null,
};
if (result?.status !== 'update_available') {
return unavailable(result?.reason || (result?.status === 'silent' ? 'no-update' : 'invalid-result'));
}
const noticeRequired = result.noticeRequired !== false;
const cached = result.source === 'cache';
const noticeText = noticeRequired
? `Archify ${result.severity === 'security' ? 'security update' : 'update'}: ${result.installedVersion} → ${result.latestVersion}. ${cached
? `A previous check at ${result.checkedAt} found a newer release.`
: 'A newer release is available.'} Release notes: ${result.releaseNotes}. The installed Skill has not changed; ask to snooze or ignore this reminder.`
: null;
return {
status: 'update_available',
installedVersion: result.installedVersion,
availableVersion: result.latestVersion,
releaseNotes: result.releaseNotes,
severity: result.severity,
checkedAt: result.checkedAt,
source: result.source,
noticeRequired,
noticeText,
...(result.eventKey ? { eventKey: result.eventKey } : {}),
...(result.reason ? { reason: result.reason } : {}),
...(result.suppressedUntil ? { suppressedUntil: result.suppressedUntil } : {}),
};
}
export function startDeliveryUpdateCheck({
env = process.env, deadlineMs = DEADLINE_MS, checkerPath = childPath,
} = {}) {
if (env.ARCHIFY_UPDATE_CHECK_DISABLED === '1') return Promise.resolve(unavailable('disabled'));
return new Promise((resolve) => {
let child;
try {
const deadlineNs = process.hrtime.bigint() + BigInt(Math.floor(deadlineMs * 1_000_000));
child = spawn(process.execPath, [checkerPath, String(deadlineNs)], {
env,
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
});
} catch {
resolve(unavailable('runtime-unavailable'));
return;
}
let output = '';
let timedOut = false;
let overflow = false;
const timer = setTimeout(() => {
timedOut = true;
child.kill('SIGKILL');
}, deadlineMs);
child.stdout.on('data', (chunk) => {
if (Buffer.byteLength(output) + chunk.length > MAX_OUTPUT_BYTES) {
overflow = true;
child.kill('SIGKILL');
} else output += chunk.toString('utf8');
});
child.on('error', () => {});
child.on('close', (code) => {
clearTimeout(timer);
if (overflow) return resolve(unavailable('invalid-result'));
// A synchronous renderer can delay both this callback and the parent's
// timer. A complete child result wins even if the timer fired meanwhile.
if (code === 0 && output.endsWith('\n')) {
try { return resolve(normalize(JSON.parse(output))); }
catch { return resolve(unavailable('invalid-result')); }
}
resolve(unavailable(timedOut || child.signalCode === 'SIGKILL' ? 'timeout' : 'check-failed'));
});
});
}
+895
View File
@@ -0,0 +1,895 @@
import { execFile } from 'node:child_process';
import { createHash, randomUUID } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { canonicalFuturePath, pathsAlias, resolveNativeOutputDirectory, resolveOutputPath } from '../renderers/shared/output-path.mjs';
import { boundedSidecarStem } from '../renderers/shared/sidecar-path.mjs';
import {
captureAtomicOutput, captureRegularFileBinding, publishRegularFileBinding,
releaseRegularFileBinding, removeOwnedRegularFile, verifyAtomicOutput,
} from '../renderers/shared/atomic-output.mjs';
import {
browserCheckSidecarPaths,
CAPTURE_VIEWPORTS,
ChromeVisualBrowser,
findChrome,
VISUAL_CHECK_VIEWPORTS,
} from './visual-check.mjs';
import { startDeliveryUpdateCheck } from './delivery-update.mjs';
export const FINALIZE_STAGES = Object.freeze(['validate', 'deliver', 'check', 'browser-check']);
const FINALIZE_UPDATE_DEADLINE_MS = 4_000;
function sha256(buffer) {
return createHash('sha256').update(buffer).digest('hex');
}
function identity(file) {
try {
const bytes = fs.readFileSync(file);
return { path: path.resolve(file), sha256: sha256(bytes), bytes: bytes.byteLength };
} catch {
return { path: path.resolve(file) };
}
}
function durationMs(start) {
return Number((process.hrtime.bigint() - start) / 1000000n);
}
function receiptWriteError(file, state) {
const error = new Error(`Could not publish finalize evidence "${file}" safely (${state.reason?.code || state.status}).`);
error.finalizeCode = 'finalize/receipt-publication';
error.finalizeEvidence = { file, state };
return error;
}
function captureReceipt(file) {
fs.mkdirSync(path.dirname(file), { recursive: true });
const capture = captureAtomicOutput(file, { requestedEntryPolicy: 'regular-or-absent' });
if (capture.status !== 'captured') throw receiptWriteError(file, capture);
return capture;
}
function writeJsonAtomic(file, value, capture, beforeCommit) {
const serialized = Buffer.from(`${JSON.stringify(value, null, 2)}\n`);
const temporary = path.join(path.dirname(capture.commitPath), `.archify-finalize-${randomUUID()}.json`);
let binding;
let stagedIdentity;
try {
fs.writeFileSync(temporary, serialized, { flag: 'wx' });
const metadata = fs.lstatSync(temporary, { bigint: true });
stagedIdentity = { device: metadata.dev, inode: metadata.ino };
if (capture.mode !== null) fs.chmodSync(temporary, capture.mode);
const staged = captureRegularFileBinding(temporary, {
subject: 'finalize-receipt', expectedIdentity: stagedIdentity,
expectedSha256: sha256(serialized), expectedBytes: serialized.byteLength,
...(capture.mode === null ? {} : { expectedMode: capture.mode }),
});
if (staged.status !== 'captured') throw receiptWriteError(file, staged);
binding = staged.binding;
beforeCommit();
const publication = publishRegularFileBinding(binding, temporary, capture.snapshot, { subject: 'finalize-receipt' });
if (!['committed', 'committed-with-warning'].includes(publication.status)) throw receiptWriteError(file, publication);
// Preserve recovery evidence instead of reporting a clean completion when cleanup failed.
if (publication.status === 'committed-with-warning') throw receiptWriteError(file, publication);
const next = captureReceipt(file);
if (!pathsAlias(next.commitPath, capture.commitPath)) {
throw receiptWriteError(file, { status: 'different', reason: { code: 'receipt-slot-changed' } });
}
return next;
} finally {
if (binding) releaseRegularFileBinding(binding);
if (stagedIdentity) removeOwnedRegularFile(temporary, stagedIdentity, { subject: 'finalize-receipt' });
}
}
function parsedReceipt(stdout) {
const source = String(stdout || '').trim();
if (!source) return null;
try { return JSON.parse(source); } catch { return null; }
}
function isReceiptObject(receipt) {
return Boolean(receipt && typeof receipt === 'object' && !Array.isArray(receipt));
}
function validIdentity(value) {
return isReceiptObject(value)
&& typeof value.sha256 === 'string'
&& /^[0-9a-f]{64}$/.test(value.sha256)
&& Number.isSafeInteger(value.bytes)
&& value.bytes >= 0;
}
function identitiesMatch(left, right) {
return validIdentity(left)
&& validIdentity(right)
&& left.sha256 === right.sha256
&& left.bytes === right.bytes;
}
function validReceiptId(value) {
return typeof value === 'string'
&& /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
}
function allChecksPassed(checks) {
return Array.isArray(checks) && checks.length > 0
&& checks.every((check) => isReceiptObject(check) && check.ok === true);
}
function validCheckComposition(receipt, quality) {
const composition = receipt?.composition;
return isReceiptObject(composition)
&& composition.schemaVersion === 1
&& composition.profile === quality
&& composition.status === 'pass'
&& composition.summary?.errors === 0
&& (quality !== 'showcase' || composition.summary?.warnings === 0);
}
function exactViewportCoverage(entries, viewports, predicate) {
if (!Array.isArray(entries) || entries.length !== viewports.length) return false;
return viewports.every((expected) => {
const matching = entries.filter((entry) => (
isReceiptObject(entry)
&& entry.width === expected.width
&& entry.height === expected.height
&& (expected.theme === undefined || entry.requestedTheme === expected.theme)
));
return matching.length === 1 && predicate(matching[0], expected);
});
}
function validBrowserEvidence(receipt) {
return exactViewportCoverage(receipt.containment?.viewports, VISUAL_CHECK_VIEWPORTS, (entry) => (
entry.theme === 'light' && entry.ok === true
))
&& exactViewportCoverage(receipt.readability?.viewports, VISUAL_CHECK_VIEWPORTS, (entry) => (
entry.theme === 'light' && entry.ok === true && entry.readabilityOk === true
))
&& exactViewportCoverage(receipt.viewerChrome?.viewports, VISUAL_CHECK_VIEWPORTS, (entry) => (
entry.theme === 'light' && entry.ok === true && entry.viewerChromeOk === true
))
&& exactViewportCoverage(receipt.themeStates?.viewports,
[...VISUAL_CHECK_VIEWPORTS.map((viewport) => ({ ...viewport, theme: 'light' })),
...CAPTURE_VIEWPORTS.map((viewport) => ({ ...viewport, theme: 'dark' }))],
(entry, expected) => entry.ok === true
&& entry.requestedTheme === expected.theme
&& entry.resolvedTheme === expected.theme);
}
function validDeliveryValidation(receipt, quality, { allowShowcaseWarnings = false } = {}) {
const validation = receipt?.validation;
return isReceiptObject(validation)
&& Number.isInteger(validation.checkCount)
&& validation.checkCount > 0
&& validation.checksPassed === validation.checkCount
&& validation.compositionStatus === 'pass'
&& validation.errors === 0
&& validation.compositionProfile === quality
&& (quality !== 'showcase' || allowShowcaseWarnings || validation.warnings === 0);
}
function validStageReceipt(stage, receipt, quality, options) {
if (!isReceiptObject(receipt) || receipt.ok !== true) return false;
if (receipt.status && receipt.status !== 'pass') return false;
if (stage === 'validate') return receipt.command === 'validate' && Array.isArray(receipt.checks);
if (stage === 'deliver') {
return receipt.command === 'deliver'
&& receipt.schemaVersion === 1
&& validReceiptId(receipt.receiptId)
&& isReceiptObject(receipt.specification)
&& validIdentity(receipt.specification)
&& validIdentity(receipt.artifact)
&& validDeliveryValidation(receipt, quality, options);
}
if (stage === 'check') {
return validIdentity(receipt.artifact)
&& allChecksPassed(receipt.checks)
&& receipt.provenance === 'current'
&& validReceiptId(receipt.deliveryReceiptId)
&& validCheckComposition(receipt, quality);
}
return receipt.command === 'browser-check'
&& receipt.schemaVersion === 1
&& receipt.status === 'pass'
&& receipt.evidenceKind === 'automated-browser'
&& validIdentity(receipt.artifact)
&& receipt.provenance === 'current'
&& validReceiptId(receipt.deliveryReceiptId)
&& receipt.containment?.status === 'pass'
&& receipt.themeStates?.status === 'pass'
&& receipt.readability?.status === 'pass'
&& receipt.viewerChrome?.status === 'pass'
&& validBrowserEvidence(receipt);
}
function identityMismatchDiagnostic({ stage, expected, actual, expectedReceiptId, actualReceiptId, output }) {
return {
code: 'finalize/artifact-binding-mismatch',
severity: 'error',
message: `The ${stage} receipt does not prove the artifact delivered by this finalize run.`,
subject: { stage, artifact: output },
evidence: {
expectedArtifact: expected,
actualArtifact: actual,
...(expectedReceiptId ? { expectedDeliveryReceiptId: expectedReceiptId } : {}),
...(actualReceiptId ? { actualDeliveryReceiptId: actualReceiptId } : {}),
},
supportedFixes: ['finish other delivery attempts for this output, then rerun finalize from the frozen candidate'],
};
}
function showcaseWarningDiagnostics(receipt) {
const issues = receipt.validation.compositionIssues;
const warnings = receipt.validation.warnings;
if (!Array.isArray(issues) || issues.length !== warnings
|| !issues.every((issue) => isReceiptObject(issue)
&& issue.severity === 'warning'
&& typeof issue.code === 'string'
&& issue.code.startsWith('composition/')
&& typeof issue.detail === 'string' && issue.detail.trim())) {
return [{
code: 'finalize/showcase-warnings', severity: 'error',
message: `Showcase delivery reported ${warnings} composition warning(s), but did not provide matching issue details.`,
subject: { stage: 'deliver', check: 'composition' },
evidence: { warnings, reportedIssues: Array.isArray(issues) ? issues.length : null },
supportedFixes: ['run validate on the frozen candidate to inspect the composition warnings, repair them, then rerun finalize'],
}];
}
return issues.map((issue) => {
const { code, severity, detail, ...evidence } = issue;
return {
code, severity: 'error',
message: `Showcase delivery reported ${code}; zero warnings are required.`,
subject: { stage: 'deliver', check: 'composition', ...(issue.nodeId ? { nodeId: issue.nodeId } : {}) },
evidence: { reportedSeverity: severity, ...evidence },
supportedFixes: [detail.replace(/^\[[^\]]+\]\s*/, '')],
};
});
}
function stageBindingDiagnostic({ stage, receipt, expectedArtifact, expectedReceiptId, output, specification, type, deliveryValidation }) {
if (stage === 'deliver') {
if (receipt.type !== type) {
return {
code: 'finalize/delivery-type-mismatch',
severity: 'error',
message: 'The delivery command reported a different diagram type than finalize requested.',
subject: { stage: 'deliver', artifact: output },
evidence: { expectedType: type, actualType: receipt.type },
supportedFixes: ['rerun finalize with a delivery command for the requested diagram type'],
};
}
if (!identitiesMatch(receipt.specification, specification)) {
return {
code: 'finalize/candidate-changed-during-delivery',
severity: 'error',
message: 'The delivery command froze a different candidate than finalize started with.',
subject: { stage: 'validate', candidate: specification.path },
evidence: {
expectedSha256: specification.sha256,
actualSha256: receipt.specification?.sha256,
expectedBytes: specification.bytes,
actualBytes: receipt.specification?.bytes,
},
supportedFixes: ['restore the frozen candidate and rerun finalize'],
};
}
if ((!receipt.output || !pathsAlias(receipt.output, output)) || !identitiesMatch(receipt.artifact, identity(output))) {
return identityMismatchDiagnostic({
stage,
expected: receipt.artifact,
actual: identity(output),
expectedReceiptId: receipt.receiptId,
output,
});
}
return null;
}
if (stage === 'check' && deliveryValidation?.checkCount !== receipt.checks.length) {
return {
code: 'finalize/delivery-validation-mismatch',
severity: 'error',
message: 'The delivery validation count does not match the complete checker receipt for the delivered artifact.',
subject: { stage: 'check', artifact: output },
evidence: {
deliveryCheckCount: deliveryValidation?.checkCount,
checkerCheckCount: receipt.checks.length,
},
supportedFixes: ['restore the complete delivery validation receipt and rerun finalize from the frozen candidate'],
};
}
const receiptArtifactPath = receipt.artifact?.path;
const receiptFile = receipt.file;
const pathMatchesOutput = stage === 'check'
? typeof receiptFile === 'string' && pathsAlias(receiptFile, output)
: typeof receiptArtifactPath === 'string' && pathsAlias(receiptArtifactPath, output);
if (!pathMatchesOutput
|| !identitiesMatch(receipt.artifact, expectedArtifact)
|| receipt.deliveryReceiptId !== expectedReceiptId) {
return identityMismatchDiagnostic({
stage,
expected: expectedArtifact,
actual: receipt.artifact,
expectedReceiptId,
actualReceiptId: receipt.deliveryReceiptId,
output,
});
}
return null;
}
function finalArtifactDiagnostic({ output, expectedArtifact, expectedReceiptId, type, deliveryPath, inspectDelivery }) {
// Recheck dev's pending/lock, physical identity, and regular-file provenance barriers at handoff.
const provenance = inspectDelivery?.(output);
if (provenance && (!provenance.ok || provenance.receiptId !== expectedReceiptId)) {
return provenance.diagnostics?.[0] || identityMismatchDiagnostic({
stage: 'finalize', output, expected: expectedArtifact,
expectedReceiptId, actualReceiptId: provenance.receiptId,
});
}
const currentArtifact = identity(output);
let delivery;
try {
delivery = JSON.parse(fs.readFileSync(deliveryPath, 'utf8'));
} catch {
delivery = null;
}
if (identitiesMatch(currentArtifact, expectedArtifact)
&& delivery?.schemaVersion === 1
&& delivery.command === 'deliver'
&& delivery.status === 'current'
&& delivery.receiptId === expectedReceiptId
&& delivery.type === type
&& typeof delivery.output === 'string' && pathsAlias(delivery.output, output)
&& identitiesMatch(delivery.artifact, expectedArtifact)) return null;
return {
code: 'finalize/final-artifact-mismatch',
severity: 'error',
message: 'The artifact or delivery receipt changed after the final gate completed.',
subject: { stage: 'finalize', artifact: output },
evidence: {
expectedArtifact,
currentArtifact,
expectedDeliveryReceiptId: expectedReceiptId,
expectedType: type,
...(delivery?.receiptId ? { currentDeliveryReceiptId: delivery.receiptId } : {}),
...(delivery?.type ? { currentType: delivery.type } : {}),
...(delivery?.artifact ? { currentDeliveryArtifact: delivery.artifact } : {}),
},
supportedFixes: ['finish other delivery attempts for this output, then rerun finalize from the frozen candidate'],
};
}
function stageStatus(stage, exitCode, receipt, quality) {
if (exitCode === 2 || receipt?.status === 'skipped') return 'skipped';
return exitCode === 0 && validStageReceipt(stage, receipt, quality) ? 'pass' : 'fail';
}
function failureDiagnostics(stage, result, receipt, quality) {
if (Array.isArray(receipt?.diagnostics) && receipt.diagnostics.length) return receipt.diagnostics;
const invalidReceipt = (result.status ?? 1) === 0 && !validStageReceipt(stage, receipt, quality);
return [{
code: invalidReceipt ? 'finalize/invalid-stage-receipt' : 'finalize/stage-failure',
severity: 'error',
message: invalidReceipt
? `The ${stage} stage exited successfully without a valid passing receipt.`
: `The ${stage} stage did not complete successfully.`,
subject: { stage },
evidence: {
exitCode: result.status ?? 1,
...(result.signal ? { signal: result.signal } : {}),
...(result.error?.message ? { reason: result.error.message } : {}),
...(String(result.stdout || '').trim() ? { stdout: String(result.stdout).trim().slice(0, 2000) } : {}),
...(String(result.stderr || '').trim() ? { stderr: String(result.stderr).trim().slice(0, 2000) } : {}),
},
supportedFixes: [invalidReceipt
? `restore the ${stage} JSON receipt contract before retrying finalize`
: 'use the compact finalize summary to repair the named subject in place, then rerun finalize once'],
}];
}
function sidecarFile(directory, value) {
if (!value) return null;
return path.resolve(directory, value);
}
function browserEvidence(receipt, artifactPath) {
if (!receipt) return {};
const directory = receipt.sidecars?.directory
? path.resolve(receipt.sidecars.directory)
: path.dirname(path.resolve(artifactPath));
return {
...(receipt.sidecars?.receipt ? { browserCheckReceipt: sidecarFile(directory, receipt.sidecars.receipt) } : {}),
};
}
function defaultRunner({ cliPath, args, cwd, env }) {
return new Promise((resolve) => {
execFile(process.execPath, [cliPath, ...args], { cwd, env, encoding: 'utf8' }, (error, stdout, stderr) => {
resolve({
status: error ? (Number.isInteger(error.code) ? error.code : 1) : 0,
stdout,
stderr,
...(error ? { error, signal: error.signal } : {}),
});
});
});
}
function stageArguments({ stage, type, input, output, quality, repoRoot, outDir }) {
const qualityArgs = ['--quality', quality];
const repoArgs = repoRoot ? ['--repo-root', repoRoot] : [];
if (stage === 'validate') return ['validate', type, input, ...qualityArgs, ...repoArgs, '--json'];
if (stage === 'deliver') return ['deliver', type, input, output, ...qualityArgs, ...repoArgs, '--json'];
if (stage === 'check') return ['check', output, '--require-provenance'];
return [
'browser-check', output, '--json', '--require-provenance',
...(outDir ? ['--out-dir', outDir] : []),
];
}
export function defaultFinalizeReceiptPath(output, { outDir } = {}) {
// Reuse the physical artifact and cross-directory namespace used by browser evidence.
const browserReceipt = browserCheckSidecarPaths(output, { outDir }).receipt;
const stem = path.basename(browserReceipt).slice(0, -'.browser-check.json'.length);
return path.join(path.dirname(browserReceipt), `${boundedSidecarStem(stem, ['.finalize.json', '.finalize-summary.json'])}.finalize.json`);
}
export function defaultFinalizeSummaryPath(receiptPath) {
const receipt = path.resolve(receiptPath);
const component = path.basename(receipt);
const isDefault = /\.finalize\.json$/i.test(component);
const stem = component.replace(isDefault ? /\.finalize\.json$/i : /\.json$/i, '');
const suffix = isDefault ? '.finalize-summary.json' : '-summary.json';
// Already bounded default stems share the receipt namespace; avoid hashing twice.
const summaryStem = Buffer.byteLength(`${stem}${suffix}`) <= 255 && `${stem}${suffix}`.length <= 255
? stem : boundedSidecarStem(stem, [suffix]);
return path.join(path.dirname(receipt), `${summaryStem}${suffix}`);
}
function defaultDeliveryPaths(output) {
const artifact = canonicalFuturePath(output);
const delivery = artifact.replace(/\.html?$/i, '.delivery.json');
return {
provenance: delivery,
pending: delivery.replace(/\.json$/i, '-pending.json'),
lock: delivery.replace(/\.json$/i, '-lock.json'),
directoryLock: path.join(path.dirname(artifact), '.archify-delivery-lock.json'),
};
}
function reservedFinalizePaths({ input, output, outDir, deliveryPaths }) {
const browser = browserCheckSidecarPaths(output, { outDir });
return [path.resolve(input), path.resolve(output), ...Object.values(deliveryPaths(output)), browser.receipt];
}
// Node moves that remove the measured crossings and detours, most specific
// first. Only positions and sizes change; every relationship keeps its endpoints.
function placementHints({ crossings = [], detours = [], crowdedSides = [] }) {
const name = (relation) => `${relation.from} → ${relation.to}`;
const other = (relation, shared) => (relation.from === shared ? relation.to : relation.from);
const hints = [];
for (const side of crowdedSides) {
hints.push(`${side.node} has ${side.relationships} relationships facing its ${side.side} side, which fits ${Math.max(1, Math.floor((side.sidePx - 32) / 14) + 1)} ports: make that side at least ${side.neededPx}px, or move some of those neighbours so they face another side of ${side.node}.`);
}
for (const crossing of crossings) {
const shared = crossing.sharedNode;
hints.push(shared
? `${name(crossing.left)} and ${name(crossing.right)} cross next to ${shared}: move node ${other(crossing.left, shared)} or ${other(crossing.right, shared)} so the two reach ${shared} from different sides (for example one level with it, one directly above or below it).`
: `${name(crossing.left)} crosses ${name(crossing.right)}: move the node of whichever is a branch, return, or second entrance to the other side of the main path, so that relationship runs through an empty corridor.`);
}
for (const detour of detours) {
if (detour.directCorridorBlockers?.length) continue;
hints.push(`${name(detour.relationship)} needs ${detour.bends} bends: move node ${detour.relationship.from} or ${detour.relationship.to} so they share a row or column with matching centers, or sit diagonally with a clear corner.`);
}
return [...new Set(hints)].slice(0, 8);
}
export function compactFinalizeReceipt(receipt) {
const gates = {};
for (const stage of FINALIZE_STAGES) gates[stage] = receipt.stages?.[stage]?.status || 'not-run';
const allDiagnostics = receipt.diagnostics || [];
const diagnosticLimit = 8;
const selectedDiagnostics = [];
const selectedIndexes = new Set();
const seenCodes = new Set();
const seenSubjects = new Set();
const subjectKey = (entry) => {
const subject = entry?.subject;
if (!subject) return null;
if (typeof subject === 'string') return subject;
if (typeof subject !== 'object' || Array.isArray(subject)) return JSON.stringify(subject);
for (const key of ['id', 'edge', 'connection', 'relationship', 'component', 'node', 'path', 'stage']) {
if (subject[key] !== undefined) return `${key}:${JSON.stringify(subject[key])}`;
}
return JSON.stringify(subject);
};
const addDiagnostic = (entry, index) => {
if (selectedDiagnostics.length >= diagnosticLimit || selectedIndexes.has(index)) return;
selectedIndexes.add(index);
selectedDiagnostics.push({
code: entry.code,
severity: entry.severity || 'error',
message: entry.message,
...(entry.subject ? { subject: entry.subject } : {}),
...(entry.evidence && Object.keys(entry.evidence).length ? { evidence: entry.evidence } : {}),
...(Array.isArray(entry.supportedFixes) && entry.supportedFixes.length
? { supportedFixes: entry.supportedFixes.slice(0, 2) } : {}),
});
};
for (const [index, entry] of allDiagnostics.entries()) {
if (seenCodes.has(entry.code)) continue;
seenCodes.add(entry.code);
addDiagnostic(entry, index);
}
for (const [index, entry] of allDiagnostics.entries()) {
const key = subjectKey(entry);
if (!key || seenSubjects.has(key)) continue;
seenSubjects.add(key);
addDiagnostic(entry, index);
}
for (const [index, entry] of allDiagnostics.entries()) addDiagnostic(entry, index);
const compact = {
schemaVersion: 1,
ok: receipt.ok,
command: 'finalize',
status: receipt.status,
type: receipt.type,
quality: receipt.quality,
specification: receipt.specification,
artifact: receipt.artifact,
gates,
...(receipt.failedStage ? { failedStage: receipt.failedStage } : {}),
diagnostics: selectedDiagnostics,
diagnosticSummary: {
total: allDiagnostics.length,
shown: selectedDiagnostics.length,
truncated: allDiagnostics.length > selectedDiagnostics.length,
},
evidence: receipt.evidence,
...(receipt.update ? { update: receipt.update } : {}),
visualReview: receipt.visualReview || 'not-requested',
durationMs: receipt.durationMs,
};
const metrics = receipt.stages?.check?.receipt?.composition?.metrics;
const reviewSignals = Object.fromEntries([
'resolvedCrossovers', 'routesOverSuggestedBends', 'routesOverSuggestedStretch',
].filter((key) => Number.isFinite(metrics?.[key]) && metrics[key] > 0)
.map((key) => [key, metrics[key]]));
if (receipt.ok && Object.keys(reviewSignals).length) {
const routeReview = receipt.stages?.check?.receipt?.composition?.routeReview;
const hints = routeReview ? placementHints(routeReview) : [];
compact.visualReviewRecommendation = {
action: 'inspect-route-readability',
signals: reviewSignals,
reason: 'Automated gates passed, but crossings or detours still need perceptual review before claiming visual quality.',
...(routeReview ? {
affectedRoutes: {
crossings: routeReview.crossings.slice(0, 8),
detours: routeReview.detours.slice(0, 8),
truncated: routeReview.crossings.length > 8 || routeReview.detours.length > 8,
},
...(hints.length ? { hints } : {}),
repair: 'Trace these relationships at the desktop viewport. For Architecture, use references/architecture-layout-repair.md: reflow a blocked main path or tangled connected scene, and repair an isolated defect locally only when the surrounding composition is accepted. Preserve all semantic content and user-fixed geometry. Rerun finalize once after the edit.',
} : {}),
};
}
const leadingSpace = receipt.stages?.check?.receipt?.composition?.leadingSpace;
if (receipt.ok && leadingSpace?.reviewSuggested === true) {
compact.layoutReviewRecommendation = {
action: 'inspect-leading-space',
evidence: leadingSpace,
reason: 'Measured content, including routes and labels, leaves a large empty area above the diagram. This is a composition suggestion, not a failed gate.',
repair: 'Check whether that leading space is intentional. If not, reposition the connected scene nearer the canvas origin while retaining room for its actual boundaries, labels and return routes. Preserve all meaning and user-fixed geometry, then rerun finalize. No screenshot is required.',
};
}
const sequenceColumnSpace = receipt.stages?.check?.receipt?.composition?.sequenceColumnSpace;
if (receipt.ok && receipt.type === 'sequence' && sequenceColumnSpace?.reviewSuggested === true) {
compact.layoutReviewRecommendation = {
action: 'inspect-sequence-width',
evidence: sequenceColumnSpace,
reason: 'Fixed participant columns leave substantial unused space on the right, after accounting for message labels and notes. This is a layout suggestion, not a failed gate.',
repair: 'For a newly authored Sequence with omitted meta.column_fit and no user-fixed column geometry, set meta.column_fit to "spread" and rerun finalize once. Preserve participant order, every message, its y position, labels, notes and sources. Retain explicit fixed layouts and legacy inputs; report the suggestion instead of changing them automatically.',
};
}
if (!receipt.ok && receipt.status === 'fail' && receipt.failedStage === 'validate') {
compact.nextAction = {
action: 'edit-in-place',
candidate: receipt.specification?.path,
constraint: receipt.type === 'architecture'
? 'Preserve all semantics and user-fixed geometry. Use references/architecture-layout-repair.md to choose a local repair or connected-scene reflow; edit the existing candidate.'
: 'Preserve unaffected semantics and geometry; do not replace the whole candidate.',
then: 'finalize-once',
};
}
return compact;
}
export async function runFinalize({
cliPath,
type,
input,
output,
quality = 'showcase',
repoRoot,
candidateSha256,
outDir,
receiptPath,
deliveryPaths = defaultDeliveryPaths,
inspectDelivery,
cwd = process.cwd(),
env = process.env,
runCommand = defaultRunner,
runBrowserCheck,
startUpdateCheck = startDeliveryUpdateCheck,
resolveChrome = findChrome,
createBrowser = (chromePath, options) => new ChromeVisualBrowser(chromePath, options),
} = {}) {
if (!cliPath || !type || !input || !output) throw new Error('finalize requires cliPath, type, input, and output.');
const started = process.hrtime.bigint();
const startedAt = new Date().toISOString();
const resolvedInput = path.resolve(input);
const resolvedOutput = resolveOutputPath({ requestedOutput: output, inputPaths: [resolvedInput] }).outputPath;
const resolvedOutDir = outDir === undefined ? undefined : resolveNativeOutputDirectory(outDir);
const specification = identity(resolvedInput);
if (candidateSha256 && specification.sha256 !== candidateSha256) {
const error = new Error(`The candidate changed after validation: expected sha256 ${candidateSha256}, found ${specification.sha256 || 'unreadable'}.`);
error.finalizeCode = 'finalize/candidate-changed';
error.finalizeEvidence = {
candidate: resolvedInput,
expectedSha256: candidateSha256,
...(specification.sha256 ? { actualSha256: specification.sha256 } : {}),
};
throw error;
}
// Validate raw native CLI paths before creating directories or normalizing them.
if (receiptPath !== undefined) resolveOutputPath({ requestedOutput: receiptPath, inputPaths: [resolvedInput], requiredExtension: '.json' });
fs.mkdirSync(path.dirname(canonicalFuturePath(resolvedOutput)), { recursive: true });
const resolvedReceipt = resolveOutputPath({
requestedOutput: receiptPath ?? defaultFinalizeReceiptPath(resolvedOutput, { outDir: resolvedOutDir }),
inputPaths: [resolvedInput], requiredExtension: '.json',
}).outputPath;
const resolvedSummary = resolveOutputPath({
requestedOutput: defaultFinalizeSummaryPath(resolvedReceipt), inputPaths: [resolvedInput], requiredExtension: '.json',
}).outputPath;
const assertReceiptPaths = () => {
const reserved = reservedFinalizePaths({ input: resolvedInput, output: resolvedOutput, outDir: resolvedOutDir, deliveryPaths });
const receiptCollision = reserved.find((file) => pathsAlias(resolvedReceipt, file));
if (receiptCollision) throw new Error(`The finalize receipt must be distinct from the specification, artifact, and gate sidecars: "${receiptCollision}".`);
const summaryCollision = [resolvedReceipt, ...reserved].find((file) => pathsAlias(resolvedSummary, file));
if (summaryCollision) throw new Error(`The finalize summary must be distinct from the full receipt, specification, artifact, and gate sidecars: "${summaryCollision}".`);
};
assertReceiptPaths();
// Capture both paths before changing either: an unsafe summary cannot leave a new full receipt behind.
let receiptCapture = captureReceipt(resolvedReceipt);
let summaryCapture = captureReceipt(resolvedSummary);
const receipt = {
schemaVersion: 1,
ok: false,
command: 'finalize',
status: 'running',
type,
quality,
startedAt,
specification,
artifact: { path: resolvedOutput },
stages: {},
diagnostics: [],
evidence: { receipt: resolvedReceipt, summaryReceipt: resolvedSummary },
visualReview: 'not-requested',
};
const persistReceipts = () => {
assertReceiptPaths();
for (const [file, capture] of [[resolvedReceipt, receiptCapture], [resolvedSummary, summaryCapture]]) {
const current = verifyAtomicOutput(capture.snapshot);
if (current.status !== 'match') throw receiptWriteError(file, current);
}
receiptCapture = writeJsonAtomic(resolvedReceipt, receipt, receiptCapture, assertReceiptPaths);
summaryCapture = writeJsonAtomic(resolvedSummary, compactFinalizeReceipt(receipt), summaryCapture, assertReceiptPaths);
};
persistReceipts();
// The gates below usually take seconds, so a slower network can finish the
// update check in parallel instead of timing out on every delivery.
const updateCheck = startUpdateCheck({ env, deadlineMs: FINALIZE_UPDATE_DEADLINE_MS });
// Only launch/attach the blank browser here. The normal browser gate still
// verifies current delivery provenance before it consumes this one-shot factory.
const chromePath = runBrowserCheck ? resolveChrome({ env }) : null;
let browser;
let browserStartupError;
let browserTransferred = false;
if (chromePath) {
try {
browser = createBrowser(chromePath, { env });
// Deliver/check may fail before inspect() awaits startup. Handle the
// rejection now while retaining the same promise for the browser gate.
browser.sessionPromise.catch(() => {});
} catch (error) {
browserStartupError = error;
}
}
const browserFactory = () => {
if (browserStartupError) throw browserStartupError;
if (browserTransferred || !browser) throw new Error('The finalize browser is unavailable or already consumed.');
browserTransferred = true;
return browser;
};
try {
let exitCode = 0;
for (const stage of ['deliver', 'check', 'browser-check']) {
const stageStarted = process.hrtime.bigint();
const args = stageArguments({
stage,
type,
input: resolvedInput,
output: resolvedOutput,
quality,
repoRoot,
outDir: resolvedOutDir,
});
const inProcess = stage === 'browser-check' && runBrowserCheck;
let result;
if (inProcess) {
const checked = await runBrowserCheck({
artifactPath: resolvedOutput,
outDir: resolvedOutDir,
chromePath,
resolveChrome: () => chromePath,
browserFactory,
});
result = { status: checked.exitCode, stdout: JSON.stringify(checked.receipt) };
} else {
result = await runCommand({ stage, cliPath, args, cwd,
env: stage === 'deliver' ? { ...env, ARCHIFY_UPDATE_CHECK_DISABLED: '1' } : env });
}
const stageReceipt = parsedReceipt(result.stdout);
const code = result.status ?? 1;
let status = stageStatus(stage, code, stageReceipt, quality);
let stageDiagnostics;
const command = [process.execPath, cliPath, ...args];
const elapsed = durationMs(stageStarted);
if (status === 'pass') {
const deliveryReceipt = stage === 'deliver' ? stageReceipt : receipt.stages.deliver?.receipt;
const bindingDiagnostic = stageBindingDiagnostic({
stage,
receipt: stageReceipt,
expectedArtifact: deliveryReceipt?.artifact,
expectedReceiptId: deliveryReceipt?.receiptId,
output: resolvedOutput,
specification,
type,
deliveryValidation: deliveryReceipt?.validation,
});
if (bindingDiagnostic) stageDiagnostics = [bindingDiagnostic];
} else if (stage === 'deliver' && code === 0 && quality === 'showcase'
&& Number.isSafeInteger(stageReceipt?.validation?.warnings)
&& stageReceipt.validation.warnings > 0
&& validStageReceipt(stage, stageReceipt, quality, { allowShowcaseWarnings: true })) {
const bindingDiagnostic = stageBindingDiagnostic({
stage, receipt: stageReceipt, output: resolvedOutput, specification, type,
});
stageDiagnostics = bindingDiagnostic ? [bindingDiagnostic] : showcaseWarningDiagnostics(stageReceipt);
}
if (stageDiagnostics) {
status = 'fail';
}
const stageEntry = {
status,
exitCode: code,
durationMs: elapsed,
command,
...(inProcess ? { execution: 'in-process' } : {}),
...(stageReceipt ? { receipt: stageReceipt } : {}),
...(!stageReceipt && String(result.stdout || '').trim() ? { stdout: String(result.stdout).trim() } : {}),
...(String(result.stderr || '').trim() ? { stderr: String(result.stderr).trim() } : {}),
...(result.signal ? { signal: result.signal } : {}),
};
if (stage === 'deliver' && status === 'pass') {
receipt.stages.validate = {
status: 'pass',
exitCode: 0,
durationMs: null,
execution: 'embedded-in-deliver',
command,
receipt: {
schemaVersion: 1,
ok: true,
command: 'validate',
specification: stageReceipt.specification,
validation: stageReceipt.validation,
},
};
receipt.stages.deliver = stageEntry;
} else if (stage === 'deliver') {
const validationFailed = stageDiagnostics?.some((diagnostic) => diagnostic.code === 'finalize/candidate-changed-during-delivery')
|| ['input', 'render', 'check'].includes(stageReceipt?.stage);
receipt.stages.validate = validationFailed ? {
...stageEntry,
status: 'fail',
durationMs: null,
execution: 'embedded-in-deliver',
} : { status: 'not-run', execution: 'embedded-in-deliver' };
receipt.stages.deliver = validationFailed
? { status: 'not-run', execution: 'blocked-by-validate' }
: stageEntry;
} else {
receipt.stages[stage] = stageEntry;
}
if (stage === 'deliver' && stageReceipt?.artifact) receipt.artifact = {
path: resolvedOutput,
...stageReceipt.artifact,
};
if (stage === 'browser-check') {
receipt.evidence = {
receipt: resolvedReceipt,
summaryReceipt: resolvedSummary,
...browserEvidence(stageReceipt, resolvedOutput),
};
}
if (status !== 'pass') {
exitCode = status === 'skipped' ? 2 : (code || 1);
receipt.status = status;
receipt.diagnostics = stageDiagnostics || failureDiagnostics(stage, result, stageReceipt, quality);
receipt.failedStage = stage === 'deliver'
&& receipt.stages.validate.status === 'fail' ? 'validate' : stage;
break;
}
persistReceipts();
}
receipt.ok = exitCode === 0;
receipt.status = receipt.ok ? 'pass' : receipt.status === 'running' ? 'fail' : receipt.status;
if (receipt.ok) {
const deliveryReceipt = receipt.stages.deliver?.receipt;
const diagnostic = finalArtifactDiagnostic({
output: resolvedOutput,
expectedArtifact: deliveryReceipt?.artifact,
expectedReceiptId: deliveryReceipt?.receiptId,
type,
deliveryPath: deliveryPaths(resolvedOutput).provenance,
inspectDelivery,
});
if (diagnostic) {
exitCode = 1;
receipt.ok = false;
receipt.status = 'fail';
receipt.failedStage = 'finalize';
receipt.diagnostics = [diagnostic];
}
}
if (receipt.ok && !identitiesMatch(specification, identity(resolvedInput))) {
exitCode = 1;
receipt.ok = false;
receipt.status = 'fail';
receipt.failedStage = 'finalize';
receipt.diagnostics = [{
code: 'finalize/candidate-changed', severity: 'error',
message: 'The frozen candidate changed before the final handoff.',
subject: { candidate: resolvedInput },
evidence: { expected: specification, actual: identity(resolvedInput) },
supportedFixes: ['restore the frozen candidate, or finalize the edited candidate as a new attempt'],
}];
}
receipt.artifact = receipt.ok
? { path: resolvedOutput, ...receipt.stages.deliver.receipt.artifact }
: identity(resolvedOutput);
receipt.finishedAt = new Date().toISOString();
receipt.durationMs = durationMs(started);
receipt.update = await updateCheck;
persistReceipts();
return { exitCode, receipt, summary: compactFinalizeReceipt(receipt) };
} finally {
if (browser && !browserTransferred) await browser.close();
await updateCheck;
}
}
+146
View File
@@ -0,0 +1,146 @@
import { spawnSync } from 'node:child_process';
import path from 'node:path';
const OPENERS = {
darwin: {
command: 'open',
method: 'open',
timeoutMs: 5000,
args: (target) => [target],
},
linux: {
command: 'xdg-open',
method: 'xdg-open',
timeoutMs: 5000,
args: (target) => [target],
},
win32: {
command: 'powershell.exe',
method: 'powershell',
// PowerShell cold starts can approach five seconds on hosted Windows
// runners. Keep the launch bounded without treating normal startup as a
// timeout.
timeoutMs: 15000,
// Keep the command constant and pass the target through a child-only
// environment variable. Paths are never interpolated into executable source.
args: () => [
'-NoProfile',
'-NonInteractive',
'-Command',
'Start-Process -FilePath $env:ARCHIFY_OPEN_TARGET',
],
},
};
function failureDetails(result, opener, timeoutMs) {
const error = result?.error;
if (error?.code === 'ENOENT') {
return {
code: 'opener/unavailable',
reason: `Could not find ${opener.command}. Install or enable the platform opener, then open the target manually.`,
systemCode: 'ENOENT',
};
}
if (error?.code === 'ETIMEDOUT') {
return {
code: 'opener/timeout',
reason: `${opener.command} did not finish within ${timeoutMs}ms. Open the target manually or retry when the system is less busy.`,
systemCode: 'ETIMEDOUT',
timeoutMs,
};
}
if (error) {
return {
code: 'opener/spawn-failed',
reason: error.message || `${opener.command} could not be started. Open the target manually.`,
...(error.code ? { systemCode: String(error.code) } : {}),
};
}
if (result?.signal) {
return {
code: 'opener/signaled',
reason: `${opener.command} was terminated by ${result.signal}. Open the target manually.`,
signal: result.signal,
};
}
if (result?.status !== 0) {
return {
code: 'opener/nonzero-exit',
reason: `${opener.command} exited with status ${result?.status ?? 'unknown'}. Open the target manually.`,
exitCode: result?.status ?? null,
};
}
return null;
}
function launchTimeout(value, fallback) {
return Number.isSafeInteger(value) && value > 0 ? value : fallback;
}
function launchTarget(target, options = {}) {
const platform = options.platform || process.platform;
const opener = OPENERS[platform];
if (!opener) {
return {
requested: true,
status: 'unsupported',
target,
method: null,
};
}
const spawn = options.spawn || spawnSync;
const timeoutMs = launchTimeout(options.timeoutMs, opener.timeoutMs);
let result;
try {
const spawnOptions = {
encoding: 'utf8',
shell: false,
stdio: 'ignore',
timeout: timeoutMs,
windowsHide: true,
};
if (platform === 'win32') {
spawnOptions.env = {
...process.env,
ARCHIFY_OPEN_TARGET: target,
};
}
result = spawn(opener.command, opener.args(target), spawnOptions);
} catch (error) {
result = { error };
}
const failure = failureDetails(result, opener, timeoutMs);
let status = 'opened';
if (failure?.code === 'opener/unavailable') status = 'unsupported';
else if (failure) status = 'failed';
return {
requested: true,
status,
target,
method: opener.method,
...(failure ? { failure } : {}),
};
}
export function openArtifact(target, options = {}) {
return launchTarget(path.resolve(target), options);
}
export function openLoopbackUrl(target, options = {}) {
let url;
try {
url = new URL(target);
} catch {
throw new TypeError('Preview URL must be a valid loopback HTTP URL.');
}
if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port) {
throw new TypeError('Preview URL must be a loopback URL using http://127.0.0.1:<port>.');
}
if (url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
throw new TypeError('Preview URL must target the loopback preview root.');
}
return launchTarget(url.href, options);
}
+969
View File
@@ -0,0 +1,969 @@
import { spawn } from 'node:child_process';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import http from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { openLoopbackUrl } from './open-artifact.mjs';
import { resolveOutputPath } from '../renderers/shared/output-path.mjs';
import { sameLocation } from '../renderers/shared/path-semantics.mjs';
import {
captureAtomicOutput,
captureRegularFileBinding,
publishRegularFileBinding,
releaseRegularFileBinding,
removeEmptyDirectoryWithRetry,
removeOwnedRegularFile,
verifyAtomicOutput,
} from '../renderers/shared/atomic-output.mjs';
const here = path.dirname(fileURLToPath(import.meta.url));
const cliPath = path.join(here, 'archify.mjs');
const loopbackHost = '127.0.0.1';
const defaultDebounceMs = 400;
const defaultPollMs = 800;
const defaultStopGraceMs = 3000;
const defaultStopKillMs = 750;
const diagramTypes = new Set(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']);
let previewCommitSequence = 0;
function sha256(value) {
return createHash('sha256').update(value).digest('hex');
}
function sourceDigest(inputPath) {
try {
const bytes = fs.readFileSync(inputPath);
return { hash: sha256(bytes), bytes, missing: false };
} catch (error) {
return { hash: `unreadable:${error.code || 'unknown'}`, bytes: null, missing: true };
}
}
function initialAuthoredOutput(inputPath) {
try {
const source = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
if (typeof source?.meta?.output === 'string') {
return source.meta.output;
}
} catch {
// An invalid initial source still gets a status shell. Its output target is
// fixed to the same fallback that `deliver` would use after repair.
}
return undefined;
}
function previewPage() {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Archify Live Preview</title>
<style>
:root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
* { box-sizing: border-box; }
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #0b111b; }
body { display: grid; grid-template-rows: auto minmax(0, 1fr); color: #e8edf5; }
header { position: relative; z-index: 2; display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 7px 12px; border-bottom: 1px solid #253248; background: rgba(11, 17, 27, .96); box-shadow: 0 8px 22px rgba(0,0,0,.18); }
.brand { font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: #9cadc6; }
#status { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; min-height: 30px; padding: 5px 10px; border: 1px solid #33435d; border-radius: 999px; background: #111b2a; font-size: 12px; white-space: nowrap; }
#status::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: #6f819d; }
body[data-state="checking"] #status::before { background: #f3b44b; box-shadow: 0 0 0 4px rgba(243,180,75,.12); }
body[data-state="verified"] #status::before { background: #45d6a8; box-shadow: 0 0 0 4px rgba(69,214,168,.12); }
body[data-state="needs-fix"] #status::before { background: #ff6f78; box-shadow: 0 0 0 4px rgba(255,111,120,.12); }
details { max-width: min(62vw, 760px); }
summary { cursor: pointer; color: #ffbdc2; font-size: 12px; }
.diagnostic { position: absolute; top: 38px; right: 12px; width: min(760px, calc(100vw - 24px)); max-height: min(44vh, 360px); overflow: auto; padding: 14px; border: 1px solid #6a3440; border-radius: 10px; background: #17131b; box-shadow: 0 14px 48px rgba(0,0,0,.42); }
pre { margin: 0 0 10px; white-space: pre-wrap; overflow-wrap: anywhere; font: 11px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; color: #f2dfe2; }
button { min-height: 32px; padding: 5px 10px; border: 1px solid #4a5d79; border-radius: 7px; background: #1a273a; color: #eef4ff; cursor: pointer; }
main { position: relative; min-height: 0; }
iframe { display: none; width: 100%; height: 100%; border: 0; background: #fff; }
body[data-has-artifact="true"] iframe { display: block; }
#empty { position: absolute; inset: 0; display: grid; place-items: center; padding: 32px; color: #91a2bc; text-align: center; background: radial-gradient(circle at 50% 38%, #15233a 0, #0b111b 55%); }
body[data-has-artifact="true"] #empty { display: none; }
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }
</style>
</head>
<body data-state="checking" data-has-artifact="false">
<header>
<span class="brand">Archify Preview</span>
<details id="failure" hidden>
<summary role="button" aria-controls="diagnostic-panel">View diagnostic</summary>
<div class="diagnostic" id="diagnostic-panel"><pre id="diagnostic"></pre><button id="copy" type="button">Copy diagnostic</button></div>
</details>
<span id="status" role="status" aria-live="polite">Checking · generation 1</span>
</header>
<main>
<div id="empty">Waiting for the first verified diagram. Invalid input will stay here with an exact diagnostic.</div>
<iframe id="artifact" title="Verified Archify diagram"></iframe>
</main>
<script>
(function () {
'use strict';
var body = document.body;
var status = document.getElementById('status');
var failure = document.getElementById('failure');
var diagnostic = document.getElementById('diagnostic');
var artifact = document.getElementById('artifact');
var lastRevision = 0;
function render(state) {
body.dataset.state = state.status;
if (state.status === 'verified') {
status.textContent = 'Verified · rev ' + state.revision;
failure.hidden = true;
failure.open = false;
if (state.revision !== lastRevision) {
lastRevision = state.revision;
artifact.src = '/artifact.html?revision=' + encodeURIComponent(state.revision) + '&sha=' + encodeURIComponent(state.lastVerified.sha256.slice(0, 12));
body.dataset.hasArtifact = 'true';
}
} else if (state.status === 'needs-fix') {
status.textContent = 'Needs fix · ' + (state.revision ? 'showing rev ' + state.revision : 'no verified revision');
diagnostic.textContent = 'Generation ' + state.generation + ' · ' + state.failure.stage + '\\n\\n' + state.failure.message;
failure.hidden = false;
} else {
status.textContent = 'Checking · generation ' + state.generation;
failure.hidden = true;
}
}
document.getElementById('copy').addEventListener('click', function () {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(diagnostic.textContent).catch(function () {});
}
});
var events = new EventSource('/events');
events.addEventListener('state', function (event) {
try { render(JSON.parse(event.data)); } catch (_) {}
});
}());
</script>
</body>
</html>`;
}
function compactMessage(value) {
let text = String(value || 'Preview build failed without a diagnostic.').trim();
const lines = text.split(/\r?\n/);
const errorLine = lines.findIndex((line) => /^Error:\s/.test(line));
if (errorLine > 0) text = lines.slice(errorLine).join('\n');
const relevant = text.split(/\r?\n/);
const stackLine = relevant.findIndex((line, index) => index > 0 && /^\s*at\s/.test(line));
if (stackLine > 0) text = relevant.slice(0, stackLine).join('\n');
return text.length > 6000 ? `${text.slice(0, 6000)}\n… diagnostic truncated` : text;
}
function redactDiagnostic(value, paths) {
let text = compactMessage(value);
for (const [absolutePath, replacement] of paths) {
if (!absolutePath) continue;
text = text.split(absolutePath).join(replacement);
}
return text;
}
function safeJson(value) {
return JSON.stringify(value).replace(/</g, '\\u003c');
}
function responseHeaders(contentType) {
return {
'Cache-Control': 'no-store',
'Content-Type': contentType,
'Cross-Origin-Resource-Policy': 'same-origin',
'Referrer-Policy': 'no-referrer',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'SAMEORIGIN',
};
}
function parseReceipt(stdout) {
try {
return JSON.parse(stdout);
} catch {
return null;
}
}
function atomicOutputFailure(result) {
const code = result?.reason?.code || 'unclassified';
let diagnosticCode = 'output/target-indeterminate';
let message;
if (code === 'target-hardlinked' || code === 'candidate-hardlinked') {
diagnosticCode = 'output/target-hardlinked';
message = code === 'candidate-hardlinked'
? 'Preview commit candidate has multiple hard-link names and cannot be published safely.'
: 'Preview output has multiple hard-link names; atomic publication cannot update every name.';
} else if (code === 'target-not-regular-file' || code === 'candidate-not-regular-file') {
diagnosticCode = 'output/target-not-regular-file';
message = code === 'candidate-not-regular-file'
? 'Preview commit candidate is no longer a regular file.'
: 'Preview output already exists and is not a regular file.';
} else if (result?.status === 'different') {
diagnosticCode = 'output/target-changed';
message = `Preview output changed while the verified candidate was being prepared (${code}).`;
} else {
message = `Preview output stability could not be determined safely (${code}).`;
}
return {
code: diagnosticCode,
message,
evidence: { relation: result?.reason || { code } },
};
}
function atomicOutputError(result) {
const failure = atomicOutputFailure(result);
return Object.assign(new Error(failure.message), { previewFailure: failure });
}
function stagePreviewCommit(commitPath, artifact, mode) {
for (let attempt = 0; attempt < 100; attempt += 1) {
previewCommitSequence += 1;
const candidatePath = path.join(
path.dirname(commitPath),
`.archify-preview-commit-${process.pid}-${Date.now().toString(36)}-${previewCommitSequence}.tmp`,
);
let descriptor;
let identity;
try {
const noFollow = process.platform === 'win32' ? 0 : (fs.constants.O_NOFOLLOW || 0);
descriptor = fs.openSync(
candidatePath,
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow,
mode ?? 0o666,
);
let metadata;
try {
metadata = fs.fstatSync(descriptor, { bigint: true });
} catch (error) {
try {
const retry = fs.fstatSync(descriptor, { bigint: true });
if (retry.isFile() && retry.ino !== 0n) {
identity = { device: retry.dev, inode: retry.ino };
}
} catch {}
throw error;
}
if (!metadata.isFile() || metadata.ino === 0n) {
throw new Error('Preview commit candidate identity could not be verified safely.');
}
identity = { device: metadata.dev, inode: metadata.ino };
fs.writeFileSync(descriptor, artifact);
if (mode !== null) fs.fchmodSync(descriptor, mode);
fs.closeSync(descriptor);
descriptor = undefined;
return { candidatePath, identity };
} catch (error) {
if (descriptor !== undefined) {
try { fs.closeSync(descriptor); } catch {}
}
if (error.code === 'EEXIST') continue;
if (identity) removeOwnedRegularFile(candidatePath, identity);
throw error;
}
}
throw Object.assign(new Error('Could not reserve a preview commit candidate beside the output.'), {
code: 'EEXIST',
errno: -17,
syscall: 'open',
});
}
function captureOwnedDirectory(directoryPath) {
const metadata = fs.lstatSync(directoryPath, { bigint: true });
if (!metadata.isDirectory() || metadata.ino === 0n) {
throw new Error('Preview staging directory identity could not be verified safely.');
}
return { device: metadata.dev, inode: metadata.ino };
}
function cleanupOwnedDirectory(directoryPath, identity) {
let current;
try {
current = fs.lstatSync(directoryPath, { bigint: true });
} catch (error) {
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return;
throw error;
}
// Preserve a replacement rather than recursively deleting a directory that
// this preview instance did not create.
if (!current.isDirectory()
|| current.ino === 0n
|| current.dev !== identity.device
|| current.ino !== identity.inode) return;
try {
removeEmptyDirectoryWithRetry(directoryPath);
} catch (error) {
// An entry whose identity was never bound to this preview may be an
// external claimant. Preserve the private directory as recovery material
// instead of recursively deleting unknown contents.
if (error?.code !== 'ENOTEMPTY' && error?.code !== 'EEXIST') throw error;
}
}
function captureOwnedDeliverySidecars(candidatePath, snapshotPath, receipt) {
const suffixes = ['.delivery.json', '.delivery-pending.json'];
const capturedSidecars = [];
for (const suffix of suffixes) {
const sidecarPath = candidatePath.replace(/\.html$/iu, suffix);
const captured = captureRegularFileBinding(sidecarPath, {
subject: 'preview-delivery-sidecar',
expectedLinks: 1,
includeContent: true,
});
if (captured.status !== 'captured') continue;
try {
const sidecar = JSON.parse(captured.content.buffer.toString('utf8'));
const commonMatches = sidecar?.schemaVersion === 1
&& sidecar.command === 'deliver'
&& sameLocation(sidecar.output, candidatePath).status === 'match'
&& (!receipt?.receiptId || sidecar.receiptId === receipt.receiptId);
const currentMatches = suffix === '.delivery.json'
&& commonMatches
&& sidecar.status === 'current'
&& receipt?.artifact?.sha256
&& sidecar.artifact?.sha256 === receipt.artifact.sha256;
const pendingMatches = suffix === '.delivery-pending.json'
&& commonMatches
&& sidecar.status === 'pending'
&& sameLocation(sidecar.input, snapshotPath).status === 'match';
if (currentMatches || pendingMatches) {
capturedSidecars.push({ path: sidecarPath, identity: captured.identity });
}
} catch {
// Preserve malformed or claimant-controlled sidecars for recovery.
} finally {
releaseRegularFileBinding(captured.binding);
}
}
return capturedSidecars;
}
export async function startPreview(options) {
const type = options.type;
if (!diagramTypes.has(type)) throw new Error(`Unknown diagram type "${type}".`);
if (options.quality && !['standard', 'showcase'].includes(options.quality)) {
throw new Error(`Unknown quality profile "${options.quality}".`);
}
const inputPath = path.resolve(options.input);
const outputRequest = {
requestedOutput: options.output,
authoredOutput: initialAuthoredOutput(inputPath),
defaultOutput: `${type}.html`,
inputPaths: [inputPath],
inputDescription: 'its JSON input',
cwd: options.cwd || process.cwd(),
};
const { outputPath } = resolveOutputPath(outputRequest);
const outputDirectory = path.dirname(outputPath);
const debounceMs = Number.isFinite(options.debounceMs) ? options.debounceMs : defaultDebounceMs;
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : defaultPollMs;
const stopGraceMs = Number.isFinite(options.stopGraceMs) ? Math.max(0, options.stopGraceMs) : defaultStopGraceMs;
const stopKillMs = Number.isFinite(options.stopKillMs) ? Math.max(0, options.stopKillMs) : defaultStopKillMs;
const shouldOpen = options.open !== false;
fs.mkdirSync(outputDirectory, { recursive: true });
// Keep cleanup bound to the physical directory selected at startup. A
// symlink or junction in the requested output parent may be redirected while
// preview is running and must not redirect recursive cleanup to a claimant.
const physicalOutputDirectory = fs.realpathSync.native(outputDirectory);
const stagingDirectory = fs.mkdtempSync(path.toNamespacedPath(
path.join(physicalOutputDirectory, '.archify-preview-'),
));
let stagingIdentity;
try {
stagingIdentity = captureOwnedDirectory(stagingDirectory);
} catch (error) {
try { fs.rmdirSync(stagingDirectory); } catch {}
throw error;
}
let port = 0;
let watcher;
let debounceTimer;
let pollTimer;
let stopGraceTimer;
let stopKillTimer;
let child;
let stopping = false;
let forceStopping = false;
let stopped = false;
let serverClosing = false;
let serverClosed = false;
let queuedHash = null;
let activeHash = null;
let lastGoodSourceHash = null;
let sourceEpoch = 0;
let activeEpoch = 0;
let pendingBuild = false;
let artifactBuffer = null;
const clients = new Set();
const sockets = new Set();
const state = {
schemaVersion: 1,
status: 'checking',
generation: 0,
revision: 0,
lastVerified: null,
failure: null,
};
let resolveClosed;
const closed = new Promise((resolve) => { resolveClosed = resolve; });
function publicState() {
return JSON.parse(JSON.stringify(state));
}
function sendState(res) {
res.write(`event: state\ndata: ${safeJson(publicState())}\n\n`);
}
function broadcast() {
for (const res of clients) sendState(res);
}
const page = Buffer.from(previewPage());
const server = http.createServer((req, res) => {
const expectedHost = `${loopbackHost}:${port}`;
if (req.headers.host !== expectedHost) {
res.writeHead(403, responseHeaders('text/plain; charset=utf-8'));
res.end('Forbidden host');
return;
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
res.writeHead(405, { ...responseHeaders('text/plain; charset=utf-8'), Allow: 'GET, HEAD' });
res.end('Method not allowed');
return;
}
let url;
try {
url = new URL(req.url, `http://${expectedHost}`);
} catch {
res.writeHead(400, responseHeaders('text/plain; charset=utf-8'));
res.end('Bad request');
return;
}
if (url.pathname === '/') {
res.writeHead(200, {
...responseHeaders('text/html; charset=utf-8'),
'Content-Security-Policy': "default-src 'none'; frame-src 'self'; connect-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'",
'Content-Length': page.byteLength,
});
if (req.method === 'HEAD') res.end();
else res.end(page);
return;
}
if (url.pathname === '/state') {
const body = Buffer.from(`${safeJson(publicState())}\n`);
res.writeHead(200, { ...responseHeaders('application/json; charset=utf-8'), 'Content-Length': body.byteLength });
if (req.method === 'HEAD') res.end();
else res.end(body);
return;
}
if (url.pathname === '/artifact.html') {
if (!artifactBuffer) {
res.writeHead(404, responseHeaders('text/plain; charset=utf-8'));
res.end('No verified artifact yet');
return;
}
res.writeHead(200, { ...responseHeaders('text/html; charset=utf-8'), 'Content-Length': artifactBuffer.byteLength });
if (req.method === 'HEAD') res.end();
else res.end(artifactBuffer);
return;
}
if (url.pathname === '/events' && req.method === 'GET') {
res.writeHead(200, {
...responseHeaders('text/event-stream; charset=utf-8'),
Connection: 'keep-alive',
});
res.write('retry: 1000\n\n');
clients.add(res);
sendState(res);
req.on('close', () => clients.delete(res));
return;
}
res.writeHead(404, responseHeaders('text/plain; charset=utf-8'));
res.end('Not found');
});
server.on('connection', (socket) => {
sockets.add(socket);
socket.once('close', () => sockets.delete(socket));
// A connection event already queued when force-stop begins must not keep
// server.close() waiting after the current sockets have been destroyed.
if (forceStopping) socket.destroy();
});
try {
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, loopbackHost, () => {
server.off('error', reject);
port = server.address().port;
resolve();
});
});
} catch (error) {
try { server.close(); } catch {}
cleanupOwnedDirectory(stagingDirectory, stagingIdentity);
throw error;
}
const url = `http://${loopbackHost}:${port}/`;
function finishStop() {
if (stopped || child || !serverClosed) return;
stopped = true;
clearTimeout(debounceTimer);
clearInterval(pollTimer);
clearTimeout(stopGraceTimer);
clearTimeout(stopKillTimer);
try {
cleanupOwnedDirectory(stagingDirectory, stagingIdentity);
} finally {
resolveClosed();
}
}
function signalActiveChild(signal) {
if (!child || child.exitCode !== null || child.signalCode !== null) return;
try {
if (process.platform !== 'win32' && child.pid) process.kill(-child.pid, signal);
else child.kill(signal);
} catch (error) {
if (error.code === 'ESRCH') return;
try { child.kill(signal); } catch {}
}
}
function closeServer() {
if (serverClosing) return;
serverClosing = true;
for (const res of clients) res.end();
clients.clear();
server.close(() => {
serverClosed = true;
finishStop();
});
server.closeIdleConnections?.();
}
function startBoundedChildDrain() {
if (!child || stopGraceTimer || stopKillTimer) return;
stopGraceTimer = setTimeout(() => {
stopGraceTimer = undefined;
if (!child) return finishStop();
signalActiveChild('SIGTERM');
stopKillTimer = setTimeout(() => {
stopKillTimer = undefined;
signalActiveChild('SIGKILL');
}, stopKillMs);
}, stopGraceMs);
}
async function stop({ force = false } = {}) {
if (force) forceStopping = true;
if (!stopping) {
stopping = true;
clearTimeout(debounceTimer);
clearInterval(pollTimer);
watcher?.close();
closeServer();
}
if (forceStopping) {
for (const socket of sockets) socket.destroy();
}
if (child && force) {
clearTimeout(stopGraceTimer);
clearTimeout(stopKillTimer);
stopGraceTimer = undefined;
stopKillTimer = undefined;
signalActiveChild('SIGKILL');
} else if (child) {
startBoundedChildDrain();
} else {
finishStop();
}
return closed;
}
function publishFailure(receipt, stdout, stderr, candidatePath, snapshotPath) {
const repairDetails = receipt?.diagnostics
?.slice(0, 12)
.map((entry) => {
const fix = entry.supportedFixes?.length ? `\nFix: ${entry.supportedFixes.join('; ')}` : '';
return `[${entry.code}] ${entry.message}${fix}`;
}) || [];
const checkerDetails = receipt?.checker?.checks
?.filter((check) => !check.ok)
.flatMap((check) => check.details || [])
.filter(Boolean)
.slice(0, 12) || [];
const diagnostic = [
receipt?.error,
...(repairDetails.length ? repairDetails : checkerDetails),
].filter(Boolean).join('\n') || stderr || stdout;
state.status = 'needs-fix';
state.failure = {
stage: receipt?.stage || 'render',
...(receipt?.code ? { code: receipt.code } : {}),
...(receipt?.evidence ? { evidence: receipt.evidence } : {}),
message: redactDiagnostic(
diagnostic,
[
[inputPath, '<input.json>'],
[outputPath, '<output.html>'],
[snapshotPath, '<input.json>'],
[candidatePath, '<candidate.html>'],
[stagingDirectory, '<preview-staging>'],
[path.resolve(here, '..'), '<archify-skill>'],
[path.resolve(options.cwd || process.cwd()), '<working-directory>'],
...(options.repoRoot ? [[path.resolve(options.repoRoot), '<repo-root>']] : []),
],
),
};
broadcast();
}
function commitCandidate(candidatePath, receipt, generationHash, outputCapture) {
let candidate;
let sourceCandidateBinding;
let sourceCandidateIdentity;
let commitCandidatePath;
let commitCandidateIdentity;
let commitCandidateBinding;
try {
const sourceCapture = captureRegularFileBinding(candidatePath, {
subject: 'candidate',
expectedSha256: receipt?.artifact?.sha256,
expectedBytes: receipt?.artifact?.bytes,
includeContent: true,
});
if (sourceCapture.status !== 'captured') throw atomicOutputError(sourceCapture);
sourceCandidateBinding = sourceCapture.binding;
sourceCandidateIdentity = sourceCapture.identity;
candidate = sourceCapture.content.buffer;
const digest = sourceCapture.content.sha256;
const releasedSource = releaseRegularFileBinding(sourceCandidateBinding);
sourceCandidateBinding = undefined;
if (releasedSource.status !== 'released') throw atomicOutputError(releasedSource);
resolveOutputPath(outputRequest);
const sameArtifact = state.lastVerified?.sha256 === digest;
const currentSource = sourceDigest(inputPath);
if (currentSource.hash !== generationHash) {
return {
committed: false,
supersededBy: currentSource,
candidateIdentity: sourceCandidateIdentity,
};
}
const beforeStage = verifyAtomicOutput(outputCapture.snapshot);
if (beforeStage.status !== 'match') throw atomicOutputError(beforeStage);
({
candidatePath: commitCandidatePath,
identity: commitCandidateIdentity,
} = stagePreviewCommit(outputCapture.commitPath, candidate, outputCapture.mode));
const candidateCapture = captureRegularFileBinding(commitCandidatePath, {
subject: 'candidate',
expectedSha256: digest,
expectedBytes: candidate.byteLength,
expectedIdentity: commitCandidateIdentity,
...(outputCapture.mode === null ? {} : { expectedMode: outputCapture.mode }),
});
if (candidateCapture.status !== 'captured') throw atomicOutputError(candidateCapture);
commitCandidateBinding = candidateCapture.binding;
const beforeCommit = verifyAtomicOutput(outputCapture.snapshot);
if (beforeCommit.status !== 'match') throw atomicOutputError(beforeCommit);
const publication = publishRegularFileBinding(
commitCandidateBinding,
commitCandidatePath,
outputCapture.snapshot,
{ subject: 'candidate' },
);
if (!['committed', 'committed-with-warning'].includes(publication.status)) {
throw atomicOutputError(publication);
}
const releasedCandidate = releaseRegularFileBinding(commitCandidateBinding);
commitCandidateBinding = undefined;
if (releasedCandidate.status !== 'released') throw atomicOutputError(releasedCandidate);
commitCandidatePath = undefined;
commitCandidateIdentity = undefined;
artifactBuffer = candidate;
lastGoodSourceHash = generationHash;
state.status = 'verified';
if (!sameArtifact) {
state.revision += 1;
state.lastVerified = {
sha256: digest,
bytes: candidate.byteLength,
checksPassed: receipt.validation.checksPassed,
checkCount: receipt.validation.checkCount,
compositionProfile: receipt.validation.compositionProfile,
compositionStatus: receipt.validation.compositionStatus,
};
}
state.failure = null;
broadcast();
return { committed: true, supersededBy: null, candidateIdentity: sourceCandidateIdentity };
} catch (error) {
publishFailure({
stage: 'commit',
error: `Could not publish the verified preview: ${error.message}`,
...(error.previewFailure || {}),
}, '', '', candidatePath);
return { committed: false, supersededBy: null, candidateIdentity: sourceCandidateIdentity };
} finally {
if (sourceCandidateBinding) releaseRegularFileBinding(sourceCandidateBinding);
if (commitCandidateBinding) releaseRegularFileBinding(commitCandidateBinding);
if (commitCandidatePath && commitCandidateIdentity) {
removeOwnedRegularFile(commitCandidatePath, commitCandidateIdentity);
}
}
}
function beginBuild(digest, epoch) {
if (stopping || child) return;
activeHash = digest.hash;
activeEpoch = epoch;
state.generation += 1;
state.status = 'checking';
state.failure = null;
broadcast();
const candidatePath = path.join(stagingDirectory, `generation-${state.generation}.html`);
const snapshotPath = path.join(stagingDirectory, `generation-${state.generation}.json`);
let snapshotIdentity;
const outputCapture = captureAtomicOutput(outputPath);
if (outputCapture.status !== 'captured') {
const failure = atomicOutputFailure(outputCapture);
publishFailure(
{ stage: 'prepare', error: failure.message, ...failure },
'',
'',
candidatePath,
snapshotPath,
);
return;
}
const beforeBuild = verifyAtomicOutput(outputCapture.snapshot);
if (beforeBuild.status !== 'match') {
const failure = atomicOutputFailure(beforeBuild);
publishFailure(
{ stage: 'prepare', error: failure.message, ...failure },
'',
'',
candidatePath,
snapshotPath,
);
return;
}
if (digest.bytes !== null) {
try {
fs.writeFileSync(snapshotPath, digest.bytes, { flag: 'wx', mode: 0o600 });
const metadata = fs.lstatSync(snapshotPath, { bigint: true });
if (metadata.isFile() && metadata.ino !== 0n) {
snapshotIdentity = { device: metadata.dev, inode: metadata.ino };
}
} catch (error) {
publishFailure(
{ stage: 'prepare', error: `Could not snapshot the observed input: ${error.message}` },
'',
'',
candidatePath,
snapshotPath,
);
return;
}
}
const args = [options.deliveryCli || cliPath, 'deliver', type, snapshotPath, candidatePath, '--json'];
if (options.quality) args.push('--quality', options.quality);
if (options.repoRoot) args.push('--repo-root', path.resolve(options.repoRoot));
let stdout = '';
let stderr = '';
child = spawn(process.execPath, args, {
cwd: options.cwd || process.cwd(),
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: process.platform !== 'win32',
});
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.on('error', (error) => { stderr += error.message; });
child.on('close', (code) => {
const receipt = parseReceipt(stdout);
const generationEpoch = activeEpoch;
const generationHash = activeHash;
const stale = generationEpoch !== sourceEpoch;
let supersededBy = null;
let candidateIdentity;
const deliverySidecars = captureOwnedDeliverySidecars(
candidatePath,
snapshotPath,
receipt,
);
child = null;
clearTimeout(stopGraceTimer);
clearTimeout(stopKillTimer);
stopGraceTimer = undefined;
stopKillTimer = undefined;
if (!stopping && !stale && code === 0 && receipt?.ok) {
({ supersededBy, candidateIdentity } = commitCandidate(
candidatePath,
receipt,
generationHash,
outputCapture,
));
} else if (code === 0 && receipt?.ok) {
const abandonedCandidate = captureRegularFileBinding(candidatePath, {
subject: 'abandoned-preview-candidate',
expectedSha256: receipt.artifact?.sha256,
expectedBytes: receipt.artifact?.bytes,
});
if (abandonedCandidate.status === 'captured') {
candidateIdentity = abandonedCandidate.identity;
releaseRegularFileBinding(abandonedCandidate.binding);
}
} else if (!stopping && !stale) {
publishFailure(receipt, stdout, stderr, candidatePath, snapshotPath);
}
if (candidateIdentity) removeOwnedRegularFile(candidatePath, candidateIdentity);
if (snapshotIdentity) removeOwnedRegularFile(snapshotPath, snapshotIdentity, { subject: 'snapshot' });
for (const deliverySidecar of deliverySidecars) {
removeOwnedRegularFile(deliverySidecar.path, deliverySidecar.identity, {
subject: 'preview-delivery-sidecar',
});
}
if (stopping) {
finishStop();
} else if (pendingBuild || stale || supersededBy) {
pendingBuild = false;
if (supersededBy && sourceEpoch === generationEpoch) sourceEpoch += 1;
const digest = supersededBy || sourceDigest(inputPath);
queueStableBuild(digest.hash, true);
}
});
}
function queueStableBuild(hash, immediate = false) {
queuedHash = hash;
clearTimeout(debounceTimer);
const launch = () => {
if (stopping) return;
const digest = sourceDigest(inputPath);
if (digest.hash !== queuedHash) {
queueStableBuild(digest.hash);
return;
}
if (digest.hash === lastGoodSourceHash) {
if (state.status !== 'verified' && state.lastVerified) {
state.status = 'verified';
state.failure = null;
broadcast();
}
return;
}
if (child) {
pendingBuild = true;
return;
}
beginBuild(digest, sourceEpoch);
};
debounceTimer = setTimeout(launch, immediate ? 0 : debounceMs);
}
function observeSource({ immediate = false } = {}) {
const digest = sourceDigest(inputPath);
if (!immediate && digest.hash === queuedHash) return;
sourceEpoch += 1;
queueStableBuild(digest.hash, immediate);
}
if (options.watch !== false) {
try {
// `path.resolve` keeps Windows 8.3 names intact. Canonicalize short names
// and junctions before libuv opens the directory so its callback path has
// the same prefix as the watched path.
const watchedDirectory = fs.realpathSync.native(path.dirname(inputPath));
watcher = fs.watch(watchedDirectory, (event, filename) => {
// On Windows the watcher hands us just the basename; on POSIX it can be
// null. Resolve named events against the directory libuv actually opened
// so file-level case, 8.3, and hard-link aliases still identify the input.
if (
!filename
|| sameLocation(path.join(watchedDirectory, filename.toString()), inputPath).status === 'match'
) {
observeSource();
}
});
watcher.on('error', () => {
const failedWatcher = watcher;
watcher = undefined;
failedWatcher?.close();
});
} catch (error) {
await stop();
throw new Error(`Could not watch the input directory: ${error.message}`);
}
}
pollTimer = setInterval(() => observeSource(), pollMs);
let opener = null;
if (shouldOpen) {
try {
opener = openLoopbackUrl(url);
} catch {
opener = { requested: true, status: 'unsupported', target: url, method: null };
}
}
observeSource({ immediate: true });
return {
url,
input: inputPath,
output: outputPath,
opener,
state: publicState,
stop,
closed,
};
}
export async function runPreview(options) {
const preview = await startPreview(options);
console.log(`preview ${preview.url}`);
console.log(`watching ${preview.input}`);
console.log(`output ${preview.output}`);
if (preview.opener && preview.opener.status !== 'opened') {
console.error(`Could not open the preview (${preview.opener.status}). ${preview.opener.failure?.reason || 'Open it manually.'} Target: ${preview.url}`);
}
let signalCount = 0;
const stop = () => {
signalCount += 1;
if (signalCount === 1) {
console.log('\nstopping preview…');
preview.stop();
} else {
console.log('\nforcing preview shutdown…');
preview.stop({ force: true });
}
};
process.on('SIGINT', stop);
process.on('SIGTERM', stop);
await preview.closed;
process.off('SIGINT', stop);
process.off('SIGTERM', stop);
}
+41
View File
@@ -0,0 +1,41 @@
import { recoverRetiredPublication } from '../renderers/shared/atomic-output.mjs';
function usage(stream = process.stderr) {
stream.write('Usage: node bin/recover-output.mjs <private-recovery-directory> [--json]\n');
}
const args = process.argv.slice(2);
if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
usage(process.stdout);
} else {
let json = false;
let recoveryDirectory;
let invalid = false;
for (const argument of args) {
if (argument === '--json' && !json) {
json = true;
} else if (argument.startsWith('-') || recoveryDirectory !== undefined) {
invalid = true;
} else {
recoveryDirectory = argument;
}
}
if (invalid || recoveryDirectory === undefined) {
usage();
process.exitCode = 64;
} else {
const result = recoverRetiredPublication(recoveryDirectory);
if (json) {
process.stdout.write(`${JSON.stringify(result)}\n`);
} else {
const reason = result.reason?.code || 'publication-recovery-unknown';
process.stdout.write(`${result.status}: ${reason}\n`);
}
// A complete prior recovery is idempotent. A preserved target is a safe,
// deliberate non-action that needs the operator to inspect the claimant.
process.exitCode = ['recovered', 'absent'].includes(result.status)
? 0
: result.status === 'preserved' ? 2 : 1;
}
}
File diff suppressed because it is too large Load Diff