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:
shancheas
2026-08-31 11:31:29 +07:00
parent 050fafd731
commit 166e0d40ac
221 changed files with 191228 additions and 1 deletions
@@ -0,0 +1,113 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-preset-tryon-'));
const CASES = {
architecture: 'web-app.architecture.json',
workflow: 'agent-tool-call.workflow.json',
sequence: 'cache-miss-request.sequence.json',
dataflow: 'product-analytics.dataflow.json',
lifecycle: 'agent-run.lifecycle.json',
};
function render(mode, preset) {
const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', CASES[mode]), 'utf8'));
if (preset === undefined) delete source.meta.visual_preset;
else source.meta.visual_preset = preset;
source.meta.animation = 'none';
const fixtureName = preset || 'default';
const input = path.join(tmp, `${mode}-${fixtureName}.json`);
const output = path.join(tmp, `${mode}-${fixtureName}.html`);
fs.writeFileSync(input, JSON.stringify(source));
execFileSync(process.execPath, [path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`), input, output]);
return fs.readFileSync(output, 'utf8');
}
function svgBlock(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
function presetRuntime(html) {
return html.match(/Archify\.preset = \(function \(\) \{[\s\S]*?\n \}\)\(\);/)?.[0] || '';
}
test('all five renderers expose one reader-controlled visual style picker', () => {
for (const mode of Object.keys(CASES)) {
const html = render(mode);
assert.match(html, /id="btn-preset"[^>]+aria-haspopup="menu"[^>]+aria-controls="preset-menu"/, mode);
assert.match(html, /id="preset-label"/, mode);
assert.match(html, /title="Choose visual style \(S cycles\)"/, mode);
assert.match(html, /id="preset-menu" role="menu" aria-label="Visual style"/, mode);
for (const preset of ['classic', 'signal-flow', 'blueprint', 'editorial']) {
assert.match(html, new RegExp(`data-preset-value="${preset}"[^>]+role="menuitemradio"`), `${mode}: ${preset}`);
}
assert.match(html, /Archify\.preset = \(function \(\)/, mode);
assert.match(html, /S -> cycle visual style/, mode);
}
});
test('style selection synchronizes page, picker, and canonical SVG without touching geometry', () => {
const html = render('architecture');
const runtime = presetRuntime(html);
assert.match(runtime, /\['classic', 'signal-flow', 'blueprint', 'editorial'\]/);
assert.match(runtime, /html\.setAttribute\('data-preset', preset\)/);
assert.match(runtime, /svg\.setAttribute\('data-preset', preset\)/);
assert.match(runtime, /data-preset-option/);
assert.match(runtime, /option\.setAttribute\('aria-checked', String\(selected\)\)/);
assert.match(runtime, /return \{ cycle: cycle, apply: apply, current: current, authored: authored, open: open, close: close, isOpen: isOpen \}/);
});
test('omitted visual preset opens as Classic and theme switching cannot change it', () => {
const html = render('architecture');
const themeRuntime = html.match(/Archify\.theme = \(function \(\) \{[\s\S]*?\n \}\)\(\);/)?.[0] || '';
assert.match(html, /<html lang="en" data-theme="dark" data-preset="classic">/);
assert.match(svgBlock(html), /<svg\b[^>]* data-preset="classic"/);
assert.match(themeRuntime, /html\.setAttribute\('data-theme', theme\)/);
assert.doesNotMatch(themeRuntime, /data-preset|Archify\.preset/);
});
test('style picker follows the accessible menu-button interaction contract', () => {
const html = render('architecture');
const runtime = presetRuntime(html);
assert.match(runtime, /function open\(focusLast\)/);
assert.match(runtime, /function close\(focusTrigger\)/);
assert.match(runtime, /e\.key === 'ArrowDown' \|\| e\.key === 'ArrowUp'/);
assert.match(runtime, /e\.key === 'Escape'/);
assert.match(runtime, /e\.key === 'Tab'/);
assert.match(runtime, /case 'Home':/);
assert.match(runtime, /case 'End':/);
assert.match(runtime, /document\.addEventListener\('click'/);
assert.match(html, /\.preset-option-swatch\.editorial/);
assert.match(
html,
/@media \(max-width: 720px\)[\s\S]*?\.toolbar \{[\s\S]*?position: relative;/,
'the mobile toolbar must preserve its stacking context so the fixed preset menu stays above guided views',
);
});
test('style try-on is session-only and unavailable to passive embeds', () => {
const html = render('workflow', 'signal-flow');
const runtime = presetRuntime(html);
assert.match(runtime, /html\.getAttribute\('data-embed'\) === 'true'/);
assert.doesNotMatch(runtime, /localStorage|sessionStorage|history\.|location\.|URLSearchParams/);
assert.match(html, /html\[data-embed="true"\] \.toolbar/);
assert.match(html, /@media print/);
});
test('same topology keeps identical canonical SVG geometry across all four styles', () => {
const normalize = (svg) => svg.replace(/ data-preset="(?:classic|signal-flow|blueprint|editorial)"/, '');
const variants = ['classic', 'signal-flow', 'blueprint', 'editorial'].map((preset) => normalize(svgBlock(render('architecture', preset))));
assert.equal(variants[1], variants[0]);
assert.equal(variants[2], variants[0]);
assert.equal(variants[3], variants[0]);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));