feat: introduce archify skill for generating architecture diagrams
- Added a new Archify skill, enabling users to create polished architecture, workflow, sequence, data-flow, and lifecycle diagrams. - Implemented comprehensive functionality including rendering, validation, and delivery of diagrams in various formats. - Integrated a user-friendly command-line interface for generating and previewing diagrams. - Developed supporting files including package.json, LICENSE, and SKILL.md for documentation and licensing. - Added unit tests to ensure reliability and functionality of the new skill. These changes enhance the application by providing a structured approach to visualizing system architecture and workflows, improving user experience and data representation.
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
ChromeVisualBrowser,
|
||||
VISUAL_CHECK_VIEWPORTS,
|
||||
chromeVisualBrowserArgs,
|
||||
runVisualCheck,
|
||||
sidecarPaths,
|
||||
} from '../bin/visual-check.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const skillRoot = path.resolve(__dirname, '..');
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-visual-check-'));
|
||||
const png = Buffer.from('89504e470d0a1a0a', 'hex');
|
||||
|
||||
function artifact(name = 'diagram.html') {
|
||||
const file = path.join(tmp, name);
|
||||
fs.writeFileSync(file, '<!doctype html><html><body>checked artifact</body></html>');
|
||||
return file;
|
||||
}
|
||||
|
||||
function sha256(file) {
|
||||
return createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
}
|
||||
|
||||
function fakeBrowser({ overflowAt, unreadableAt, chromeCollisionAt, stageCollisionAt, stageGapAt, screenshotFailure } = {}) {
|
||||
const calls = [];
|
||||
return {
|
||||
calls,
|
||||
async inspect({ width, height, theme, screenshotPath }) {
|
||||
calls.push({ width, height, theme, screenshotPath });
|
||||
if (screenshotPath && screenshotFailure?.({ width, height, theme })) {
|
||||
throw new Error('synthetic screenshot failure');
|
||||
}
|
||||
if (screenshotPath) fs.writeFileSync(screenshotPath, png);
|
||||
const overflow = overflowAt?.({ width, height, theme }) || false;
|
||||
const unreadable = unreadableAt?.({ width, height, theme }) || false;
|
||||
const chromeCollision = chromeCollisionAt?.({ width, height, theme }) || false;
|
||||
const stageCollision = stageCollisionAt?.({ width, height, theme }) || false;
|
||||
const dockStageGap = stageGapAt?.({ width, height, theme }) ?? (stageCollision ? -12 : 10);
|
||||
const stageClearanceFailure = stageCollision || dockStageGap < 10;
|
||||
return {
|
||||
innerWidth: width,
|
||||
innerHeight: height,
|
||||
scrollWidth: width + (overflow ? 1 : 0),
|
||||
scrollHeight: height,
|
||||
resolvedTheme: theme,
|
||||
readerWidth: 960,
|
||||
diagramWidth: 930,
|
||||
viewBoxWidth: 1300,
|
||||
minimumProjectedNodeTextPx: unreadable ? 5.72 : 6.44,
|
||||
minimumProjectedNodeText: unreadable ? 'Compact node' : 'Readable node',
|
||||
minimumProjectedNodeTextDetail: unreadable ? 'primary' : 'context',
|
||||
hasLegend: true,
|
||||
hasNavigationDock: true,
|
||||
legendDockIntersectionArea: chromeCollision ? 42 : 0,
|
||||
dockStageIntersectionArea: stageCollision ? 84 : 0,
|
||||
dockStageGap,
|
||||
viewerChromeRequiredGap: 10,
|
||||
viewerChromeReserve: chromeCollision || stageClearanceFailure ? 0 : 44,
|
||||
viewerChromeActive: !chromeCollision && !stageClearanceFailure,
|
||||
};
|
||||
},
|
||||
async close() {},
|
||||
};
|
||||
}
|
||||
|
||||
function fakeChromeChild() {
|
||||
const child = new EventEmitter();
|
||||
child.exitCode = null;
|
||||
child.signalCode = null;
|
||||
child.stderr = new PassThrough();
|
||||
child.stdio = [null, null, child.stderr, new PassThrough(), new PassThrough()];
|
||||
child.kill = (signal) => {
|
||||
child.signalCode = signal;
|
||||
queueMicrotask(() => {
|
||||
child.emit('exit', null, signal);
|
||||
child.emit('close', null, signal);
|
||||
});
|
||||
return true;
|
||||
};
|
||||
return child;
|
||||
}
|
||||
|
||||
test('visual-check disables the Chrome sandbox only for root or an explicit environment opt-in', () => {
|
||||
const profileRoot = path.join(tmp, 'chrome-profile');
|
||||
const ordinary = chromeVisualBrowserArgs(profileRoot, { env: {}, getuid: () => 1001 });
|
||||
const optedIn = chromeVisualBrowserArgs(profileRoot, {
|
||||
env: { ARCHIFY_CHROME_NO_SANDBOX: '1' },
|
||||
getuid: () => 1001,
|
||||
});
|
||||
const root = chromeVisualBrowserArgs(profileRoot, { env: {}, getuid: () => 0 });
|
||||
|
||||
assert.equal(ordinary.includes('--no-sandbox'), false);
|
||||
assert.equal(optedIn.includes('--no-sandbox'), true);
|
||||
assert.equal(root.includes('--no-sandbox'), true);
|
||||
});
|
||||
|
||||
test('visual-check converts a Chrome DevTools pipe reset and captured stderr into a structured failure', async () => {
|
||||
const input = artifact('chrome-pipe-reset.html');
|
||||
const child = fakeChromeChild();
|
||||
|
||||
const result = await runVisualCheck({
|
||||
artifactPath: input,
|
||||
chromePath: '/fake/chrome',
|
||||
browserFactory: async () => {
|
||||
const browser = new ChromeVisualBrowser('/fake/chrome', {
|
||||
env: { ARCHIFY_CHROME_NO_SANDBOX: '1' },
|
||||
getuid: () => 1001,
|
||||
spawnImpl: () => child,
|
||||
});
|
||||
setImmediate(() => {
|
||||
child.stderr.write('Chrome sandbox initialization failed\n');
|
||||
const error = new Error('read ECONNRESET');
|
||||
error.code = 'ECONNRESET';
|
||||
child.stdio[4].emit('error', error);
|
||||
});
|
||||
return browser;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 1);
|
||||
assert.equal(result.receipt.status, 'fail');
|
||||
assert.match(result.receipt.error, /Chrome DevTools read pipe failed/);
|
||||
assert.match(result.receipt.error, /ECONNRESET/);
|
||||
assert.match(result.receipt.error, /Chrome sandbox initialization failed/);
|
||||
assert.equal(result.receipt.diagnostics[0]?.code, 'viewer/visual-check-runtime');
|
||||
assert.match(result.receipt.diagnostics[0]?.evidence?.reason || '', /ECONNRESET/);
|
||||
assert.equal(fs.existsSync(sidecarPaths(input).receipt), true);
|
||||
});
|
||||
|
||||
test('visual-check reports Chrome early exit status and stderr without an uncaught exception', async () => {
|
||||
const input = artifact('chrome-early-exit.html');
|
||||
const child = fakeChromeChild();
|
||||
|
||||
const result = await runVisualCheck({
|
||||
artifactPath: input,
|
||||
chromePath: '/fake/chrome',
|
||||
browserFactory: async () => {
|
||||
const browser = new ChromeVisualBrowser('/fake/chrome', {
|
||||
env: { ARCHIFY_CHROME_NO_SANDBOX: '1' },
|
||||
getuid: () => 1001,
|
||||
spawnImpl: () => child,
|
||||
});
|
||||
setImmediate(() => {
|
||||
child.stderr.write('Chrome rejected its launch flags\n');
|
||||
child.exitCode = 23;
|
||||
child.emit('close', 23, null);
|
||||
});
|
||||
return browser;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 1);
|
||||
assert.equal(result.receipt.status, 'fail');
|
||||
assert.match(result.receipt.error, /Chrome DevTools process exit failed/);
|
||||
assert.match(result.receipt.error, /exit code 23/);
|
||||
assert.match(result.receipt.error, /Chrome rejected its launch flags/);
|
||||
assert.equal(result.receipt.diagnostics[0]?.code, 'viewer/visual-check-runtime');
|
||||
});
|
||||
|
||||
test('visual-check records four containment viewports and four endpoint theme captures', async () => {
|
||||
const input = artifact('passing.html');
|
||||
const before = sha256(input);
|
||||
const browser = fakeBrowser();
|
||||
const result = await runVisualCheck({
|
||||
artifactPath: input,
|
||||
chromePath: '/fake/chrome',
|
||||
browserFactory: async () => browser,
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(result.receipt.status, 'pass');
|
||||
assert.deepEqual(result.receipt.diagnostics, []);
|
||||
assert.equal(result.receipt.visualReview, 'pending');
|
||||
assert.equal(result.receipt.viewerChrome.status, 'pass');
|
||||
assert.equal(result.receipt.containment.viewports.length, VISUAL_CHECK_VIEWPORTS.length);
|
||||
assert.equal(result.receipt.containment.viewports.every((entry) => entry.ok), true);
|
||||
assert.deepEqual(
|
||||
result.receipt.captures.screenshots.map(({ width, height, theme }) => [width, height, theme]),
|
||||
[
|
||||
[1440, 900, 'light'],
|
||||
[1440, 900, 'dark'],
|
||||
[2048, 1320, 'light'],
|
||||
[2048, 1320, 'dark'],
|
||||
],
|
||||
);
|
||||
assert.equal(result.receipt.artifact.sha256, before);
|
||||
assert.equal(sha256(input), before, 'visual-check mutated the delivered artifact');
|
||||
|
||||
const outputs = sidecarPaths(input);
|
||||
assert.equal(fs.existsSync(outputs.receipt), true);
|
||||
assert.equal(fs.existsSync(outputs.contactSheet), true);
|
||||
assert.equal(outputs.screenshots.every((entry) => fs.existsSync(entry.path)), true);
|
||||
const contactSheet = fs.readFileSync(outputs.contactSheet, 'utf8');
|
||||
for (const screenshot of outputs.screenshots) {
|
||||
assert.match(contactSheet, new RegExp(path.basename(screenshot.path).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||
assert.doesNotMatch(contactSheet, new RegExp(screenshot.path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||
}
|
||||
});
|
||||
|
||||
test('visual-check returns 1 and preserves evidence when any viewport overflows', async () => {
|
||||
const input = artifact('overflow.html');
|
||||
const result = await runVisualCheck({
|
||||
artifactPath: input,
|
||||
chromePath: '/fake/chrome',
|
||||
browserFactory: async () => fakeBrowser({
|
||||
overflowAt: ({ width, theme }) => width === 1600 && theme === 'light',
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 1);
|
||||
assert.equal(result.receipt.status, 'fail');
|
||||
assert.equal(result.receipt.containment.status, 'fail');
|
||||
assert.deepEqual(
|
||||
result.receipt.containment.viewports.filter((entry) => !entry.ok).map((entry) => [entry.width, entry.height]),
|
||||
[[1600, 1000]],
|
||||
);
|
||||
const diagnostic = result.receipt.diagnostics.find(
|
||||
(entry) => entry.code === 'viewer/viewport-overflow',
|
||||
);
|
||||
assert.deepEqual(diagnostic?.subject, {
|
||||
artifact: input,
|
||||
viewport: { width: 1600, height: 1000, theme: 'light' },
|
||||
});
|
||||
assert.equal(diagnostic?.evidence?.scrollWidth, 1601);
|
||||
assert.equal(fs.existsSync(sidecarPaths(input).contactSheet), true);
|
||||
});
|
||||
|
||||
test('visual-check returns 1 when the real reader projects node text below 6px', async () => {
|
||||
const input = artifact('unreadable.html');
|
||||
const result = await runVisualCheck({
|
||||
artifactPath: input,
|
||||
chromePath: '/fake/chrome',
|
||||
browserFactory: async () => fakeBrowser({
|
||||
unreadableAt: ({ width, height, theme }) => width === 1440 && height === 900 && theme === 'light',
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 1);
|
||||
assert.equal(result.receipt.status, 'fail');
|
||||
assert.equal(result.receipt.readability.status, 'fail');
|
||||
const desktop = result.receipt.readability.viewports.find(
|
||||
(entry) => entry.width === 1440 && entry.height === 900,
|
||||
);
|
||||
assert.equal(desktop?.diagramWidth, 930);
|
||||
assert.equal(desktop?.minimumProjectedNodeText, 'Compact node');
|
||||
assert.equal(desktop?.minimumProjectedNodeTextDetail, 'primary');
|
||||
assert.equal(desktop?.readabilityOk, false);
|
||||
const diagnostic = result.receipt.diagnostics.find(
|
||||
(entry) => entry.code === 'viewer/projected-text-readability',
|
||||
);
|
||||
assert.equal(diagnostic?.evidence?.text, 'Compact node');
|
||||
assert.equal(diagnostic?.evidence?.minimumProjectedNodeTextPx, 5.72);
|
||||
assert.equal(diagnostic?.evidence?.minimumRequiredNodeTextPx, 6);
|
||||
});
|
||||
|
||||
test('visual-check returns 1 when the navigation dock obscures the SVG legend', async () => {
|
||||
const input = artifact('viewer-chrome-collision.html');
|
||||
const result = await runVisualCheck({
|
||||
artifactPath: input,
|
||||
chromePath: '/fake/chrome',
|
||||
browserFactory: async () => fakeBrowser({
|
||||
chromeCollisionAt: ({ width, height, theme }) => (
|
||||
width === 1920 && height === 1080 && theme === 'light'
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 1);
|
||||
assert.equal(result.receipt.status, 'fail');
|
||||
assert.equal(result.receipt.viewerChrome.status, 'fail');
|
||||
const desktop = result.receipt.viewerChrome.viewports.find(
|
||||
(entry) => entry.width === 1920 && entry.height === 1080,
|
||||
);
|
||||
assert.equal(desktop?.legendDockIntersectionArea, 42);
|
||||
assert.equal(desktop?.viewerChromeOk, false);
|
||||
const diagnostic = result.receipt.diagnostics.find(
|
||||
(entry) => entry.code === 'viewer/chrome-legend-clearance',
|
||||
);
|
||||
assert.equal(diagnostic?.evidence?.legendDockIntersectionArea, 42);
|
||||
});
|
||||
|
||||
test('visual-check returns 1 when the navigation dock enters the SVG stage', async () => {
|
||||
const input = artifact('viewer-stage-collision.html');
|
||||
const result = await runVisualCheck({
|
||||
artifactPath: input,
|
||||
chromePath: '/fake/chrome',
|
||||
browserFactory: async () => fakeBrowser({
|
||||
stageCollisionAt: ({ width, height, theme }) => (
|
||||
width === 1920 && height === 1080 && theme === 'light'
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 1);
|
||||
assert.equal(result.receipt.status, 'fail');
|
||||
assert.equal(result.receipt.viewerChrome.status, 'fail');
|
||||
const desktop = result.receipt.viewerChrome.viewports.find(
|
||||
(entry) => entry.width === 1920 && entry.height === 1080,
|
||||
);
|
||||
assert.equal(desktop?.dockStageIntersectionArea, 84);
|
||||
assert.equal(desktop?.dockStageGap, -12);
|
||||
assert.equal(desktop?.requiredDockStageGap, 10);
|
||||
assert.equal(desktop?.viewerChromeStageOk, false);
|
||||
assert.equal(desktop?.viewerChromeOk, false);
|
||||
const diagnostic = result.receipt.diagnostics.find(
|
||||
(entry) => entry.code === 'viewer/chrome-stage-clearance',
|
||||
);
|
||||
assert.deepEqual(diagnostic?.subject, {
|
||||
artifact: input,
|
||||
viewport: { width: 1920, height: 1080, theme: 'light' },
|
||||
});
|
||||
assert.deepEqual(diagnostic?.evidence, {
|
||||
dockStageIntersectionArea: 84,
|
||||
dockStageGap: -12,
|
||||
requiredDockStageGap: 10,
|
||||
});
|
||||
assert.match(diagnostic?.message || '', /enters the protected SVG stage/);
|
||||
assert.ok(diagnostic?.supportedFixes.some((fix) => fix.includes('dockStageGap')));
|
||||
assert.equal(diagnostic?.supportedFixes.some((fix) => fix.includes('regenerate')), false);
|
||||
});
|
||||
|
||||
test('visual-check describes insufficient stage clearance without claiming an overlap', async () => {
|
||||
const input = artifact('viewer-stage-low-gap.html');
|
||||
const result = await runVisualCheck({
|
||||
artifactPath: input,
|
||||
chromePath: '/fake/chrome',
|
||||
browserFactory: async () => fakeBrowser({
|
||||
stageGapAt: ({ width, height, theme }) => (
|
||||
width === 1920 && height === 1080 && theme === 'light' ? 5 : 10
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 1);
|
||||
const diagnostic = result.receipt.diagnostics.find(
|
||||
(entry) => entry.code === 'viewer/chrome-stage-clearance',
|
||||
);
|
||||
assert.equal(diagnostic?.evidence?.dockStageIntersectionArea, 0);
|
||||
assert.equal(diagnostic?.evidence?.dockStageGap, 5);
|
||||
assert.match(diagnostic?.message || '', /clearance.*below the required gap/i);
|
||||
assert.doesNotMatch(diagnostic?.message || '', /enters/i);
|
||||
});
|
||||
|
||||
test('visual-check returns 1 and removes misleading capture sidecars on screenshot failure', async () => {
|
||||
const input = artifact('capture-failure.html');
|
||||
const outputs = sidecarPaths(input);
|
||||
fs.writeFileSync(outputs.contactSheet, 'stale');
|
||||
for (const screenshot of outputs.screenshots) fs.writeFileSync(screenshot.path, png);
|
||||
|
||||
const result = await runVisualCheck({
|
||||
artifactPath: input,
|
||||
chromePath: '/fake/chrome',
|
||||
browserFactory: async () => fakeBrowser({
|
||||
screenshotFailure: ({ theme }) => theme === 'dark',
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 1);
|
||||
assert.equal(result.receipt.status, 'fail');
|
||||
assert.equal(result.receipt.captures.status, 'fail');
|
||||
assert.match(result.receipt.error, /synthetic screenshot failure/);
|
||||
assert.equal(result.receipt.diagnostics[0]?.code, 'viewer/visual-check-runtime');
|
||||
assert.match(result.receipt.diagnostics[0]?.evidence?.reason || '', /synthetic screenshot failure/);
|
||||
assert.equal(fs.existsSync(outputs.contactSheet), false);
|
||||
assert.equal(outputs.screenshots.some((entry) => fs.existsSync(entry.path)), false);
|
||||
assert.equal(fs.existsSync(outputs.receipt), true);
|
||||
});
|
||||
|
||||
test('visual-check returns 2 with a truthful skipped receipt when Chrome is unavailable', async () => {
|
||||
const input = artifact('no-chrome.html');
|
||||
const result = await runVisualCheck({
|
||||
artifactPath: input,
|
||||
resolveChrome: () => null,
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 2);
|
||||
assert.equal(result.receipt.status, 'skipped');
|
||||
assert.equal(result.receipt.containment.status, 'skipped');
|
||||
assert.equal(result.receipt.viewerChrome.status, 'skipped');
|
||||
assert.equal(result.receipt.captures.status, 'skipped');
|
||||
assert.equal(result.receipt.visualReview, 'pending');
|
||||
assert.equal(result.receipt.diagnostics[0]?.code, 'viewer/chrome-unavailable');
|
||||
assert.ok(result.receipt.diagnostics[0]?.supportedFixes.some((fix) => fix.includes('ARCHIFY_CHROME')));
|
||||
assert.equal(fs.existsSync(sidecarPaths(input).receipt), true);
|
||||
});
|
||||
|
||||
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
Reference in New Issue
Block a user