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,86 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
DESKTOP_READABILITY_VIEWPORT,
DESKTOP_READER_DIAGRAM_WIDTH,
DESKTOP_READER_HORIZONTAL_CHROME,
DESKTOP_READER_MIN_WIDTH,
MIN_PROJECTED_NODE_TEXT_PX,
minimumReadableSourceTextPx,
projectedNodeTextPx,
} from '../renderers/shared/desktop-readability.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
const skill = fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8');
const reader = template.slice(
template.indexOf('Adaptive Reader Shell'),
template.indexOf('Archify.view = (function ()'),
);
test('wide desktop diagrams use one height-budgeted reader shell instead of breakpoint jumps', () => {
assert.match(template, /max-width: var\(--archify-reader-width, 1440px\)/);
assert.doesNotMatch(template, /@media \(min-width: 1680px\)[\s\S]{0,180}\.container/);
assert.doesNotMatch(template, /@media \(min-width: 1920px\)[\s\S]{0,180}\.container/);
assert.match(reader, /var WIDE_RATIO = 1\.55/);
assert.match(reader, /var MAX_READER_WIDTH = 1920/);
assert.match(reader, /var availableSvgHeight = Math\.max\(1, window\.innerHeight - fixedHeight\)/);
assert.match(reader, /var desiredWidth = availableSvgHeight \* ratio \+ chrome\.diagramX/);
assert.match(reader, /html\.style\.setProperty\('--archify-reader-width', rounded \+ 'px'\)/);
});
test('desktop readability budget matches the minimum adaptive reader at 1440 by 900', () => {
assert.deepEqual(DESKTOP_READABILITY_VIEWPORT, { width: 1440, height: 900 });
assert.equal(DESKTOP_READER_MIN_WIDTH, 960);
assert.equal(DESKTOP_READER_HORIZONTAL_CHROME, 30);
assert.equal(DESKTOP_READER_DIAGRAM_WIDTH, 930);
assert.match(reader, new RegExp(`var MIN_READER_WIDTH = ${DESKTOP_READER_MIN_WIDTH}`));
assert.match(template, /html\[data-nav-stage-rail="true"\] body \{ padding-block: 0\.375rem; \}/);
assert.match(template, /@media \(min-width: 768px\) and \(max-height: 1100px\)[\s\S]*?\.diagram-container \{[\s\S]*?padding: 0\.875rem;[\s\S]*?padding-bottom: calc\(0\.875rem \+ var\(--archify-nav-reserve\)\);/);
assert.match(template, /@media \(min-width: 768px\) and \(max-height: 920px\)[\s\S]*?body \{ padding-block: 1\.25rem; \}/);
assert.match(template, /\.diagram-container \{[\s\S]*?border: 1px solid var\(--panel-border\)/);
});
test('desktop readability source floor is the inverse of the projected-size gate', () => {
const sourceFloor = minimumReadableSourceTextPx(1376);
assert.ok(Math.abs(sourceFloor - 8.87741935483871) < 1e-12);
assert.ok(Math.abs(projectedNodeTextPx(sourceFloor, 1376) - MIN_PROJECTED_NODE_TEXT_PX) < 1e-12);
assert.equal(minimumReadableSourceTextPx(DESKTOP_READER_DIAGRAM_WIDTH), MIN_PROJECTED_NODE_TEXT_PX);
assert.equal(minimumReadableSourceTextPx(700), MIN_PROJECTED_NODE_TEXT_PX);
assert.ok(Number.isNaN(minimumReadableSourceTextPx(0)));
});
test('adaptive width preserves canonical SVG geometry and yields to specialized viewer modes', () => {
assert.match(reader, /window\.innerWidth >= MIN_DESKTOP_WIDTH/);
assert.match(reader, /html\.getAttribute\('data-embed'\) !== 'true'/);
assert.match(reader, /html\.getAttribute\('data-present'\) !== 'true'/);
assert.match(reader, /window\.matchMedia\('print'\)\.matches/);
assert.doesNotMatch(reader, /svg\.setAttribute\(['"](?:viewBox|width|height)/);
assert.doesNotMatch(reader, /svg\.style\.(?:width|height)/);
assert.doesNotMatch(reader, /overflow\s*=\s*['"]hidden/);
});
test('reader remeasures real content and reduces width before allowing desktop page overflow', () => {
assert.match(reader, /document\.fonts\.ready\.then\(schedule\)/);
assert.match(reader, /new ResizeObserver\(schedule\)/);
assert.match(reader, /new MutationObserver\(schedule\)/);
assert.match(reader, /document\.documentElement\.scrollHeight/);
assert.match(reader, /lastWidth - overflow \* ratio - 4/);
assert.match(skill, /1440×900, 1600×1000, and 1920×1080/);
assert.match(skill, /2048×1320/);
assert.match(skill, /Generate one responsive artifact for laptops and external displays/);
assert.match(skill, /preserve the authored SVG\/viewBox, proportions, semantic geometry/);
});
test('reader exposes an explicit stable-dimensions contract for browser evidence', () => {
assert.match(reader, /function stableSnapshot\(\)/);
assert.match(reader, /function whenStable\(\)/);
assert.match(reader, /document\.fonts && document\.fonts\.ready/);
assert.match(reader, /Math\.ceil\(document\.body\.scrollHeight\)/);
assert.match(reader, /stableFrames >= 3/);
assert.match(reader, /whenStable: whenStable/);
});
@@ -0,0 +1,179 @@
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-animation-'));
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',
};
const NODE_COLLECTION = {
architecture: 'components',
workflow: 'nodes',
sequence: 'participants',
dataflow: 'nodes',
lifecycle: 'states',
};
function render(mode, example, animation = 'trace', visualPreset) {
const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', example), 'utf8'));
if (animation) doc.meta = { ...doc.meta, animation };
else delete doc.meta.animation;
if (visualPreset) doc.meta.visual_preset = visualPreset;
else if (visualPreset === null) delete doc.meta.visual_preset;
const suffix = `${animation || 'static'}-${visualPreset || 'default'}`;
const input = path.join(tmp, `${mode}-${suffix}.json`);
const output = path.join(tmp, `${mode}-${suffix}.html`);
fs.writeFileSync(input, JSON.stringify(doc));
execFileSync('node', [path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`), input, output], {
stdio: ['ignore', 'ignore', 'pipe'],
});
return fs.readFileSync(output, 'utf8');
}
function svgBlock(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('static output omits animation attributes', () => {
const svg = svgBlock(render('workflow', CASES.workflow, null, null));
assert.doesNotMatch(svg, /data-animation=/);
assert.doesNotMatch(svg, /data-animate=/);
});
test('classic preset remains the default for existing diagrams', () => {
const html = render('architecture', CASES.architecture, null, null);
assert.match(html, /<html lang="en" data-theme="dark" data-preset="classic">/);
assert.match(svgBlock(html), /data-preset="classic"/);
});
test('signal-flow preset reaches the page, SVG, and motion export surface', () => {
const html = render('workflow', CASES.workflow, 'trace', 'signal-flow');
assert.match(html, /<html lang="en" data-theme="dark" data-preset="signal-flow">/);
assert.match(svgBlock(html), /data-preset="signal-flow"/);
assert.match(html, /content: attr\(data-preset-badge-signal-flow\)/);
assert.match(html, /data-preset-badge-signal-flow="SIGNAL FLOW"/);
assert.match(html, /data-format="webm"/);
assert.match(html, /data-last-motion-bytes/);
assert.match(html, /Archify\.motion = \{ canRecord: canRecordMotion, recordWebm: recordWebm \}/);
assert.match(html, /recorder\.requestData\(\)/);
assert.match(html, /aria-label="Diagram view controls"/);
assert.match(html, /Archify\.focus = \(function \(\)/);
assert.match(html, /Archify\.view = \(function \(\)/);
assert.match(html, /clone\.style\.removeProperty\('transform'\)/);
assert.match(html, /clone\.removeAttribute\('data-view-scale'\)/);
assert.match(html, /data-last-export-canonical/);
assert.match(html, /data-last-export-error-format/);
assert.match(html, /data-last-export-error/);
assert.match(html, /WebM unavailable in this browser/);
assert.match(html, /Motion capture unavailable in this browser/);
assert.match(html, /canonicalStateClean: canonicalStateClean/);
assert.match(html, /recordExportReceipt\('svg', blob, d\.canonicalStateClean\)/);
});
test('webm renders an explicit time-varying canvas scene instead of replaying one cached SVG bitmap', () => {
const html = render('architecture', CASES.architecture, 'trace', 'signal-flow');
const recordBlock = html.match(/function recordWebm\(options\) \{[\s\S]*?\n var menu =/)?.[0] || '';
assert.match(recordBlock, /var motionScene = createMotionScene\(svg\)/);
assert.match(recordBlock, /drawMotionFrame\(ctx, backgroundImage, motionScene, elapsed\)/);
assert.match(recordBlock, /getPointAtLength/);
assert.match(recordBlock, /performance\.now\(\)/);
assert.doesNotMatch(
recordBlock,
/function draw\(\) \{[\s\S]*?ctx\.drawImage\(img, 0, 0, canvas\.width, canvas\.height\);[\s\S]*?requestAnimationFrame\(draw\)/,
);
});
test('blueprint preset reaches every visual surface without changing the default', () => {
const html = render('architecture', CASES.architecture, null, 'blueprint');
assert.match(html, /<html lang="en" data-theme="dark" data-preset="blueprint">/);
assert.match(svgBlock(html), /data-preset="blueprint"/);
assert.match(html, /content: attr\(data-preset-badge-blueprint\)/);
assert.match(html, /data-preset-badge-blueprint="BLUEPRINT \/ REV 01"/);
assert.match(html, /\[data-preset="blueprint"\]\[data-theme="dark"\]/);
assert.match(html, /svg\[data-preset="blueprint"\] \.c-grid/);
assert.match(html, /html\[data-preset="blueprint"\] \.guided-views/);
assert.match(html, /html\[data-preset="blueprint"\] \.card/);
});
test('blueprint preset is accepted by all five typed renderers', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example, null, 'blueprint');
assert.match(html, /data-preset="blueprint"/, mode);
assert.match(svgBlock(html), /data-preset="blueprint"/, mode);
}
});
test('editorial preset reaches every visual surface and all five typed renderers', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example, null, 'editorial');
assert.match(html, /<html lang="en" data-theme="dark" data-preset="editorial">/, mode);
assert.match(svgBlock(html), /data-preset="editorial"/, mode);
assert.match(html, /content: attr\(data-preset-badge-editorial\)/, mode);
assert.match(html, /data-preset-badge-editorial="EDITORIAL \/ FIELD NOTE"/, mode);
assert.match(html, /content: attr\(data-preset-badge-editorial-plate\)/, mode);
assert.match(html, /data-preset-badge-editorial-plate="ARCHIFY \/ PLATE 04"/, mode);
assert.match(html, /\[data-preset="editorial"\]\[data-theme="dark"\]/, mode);
assert.match(html, /html\[data-preset="editorial"\] \.diagram-container/, mode);
assert.match(html, /svg\[data-preset="editorial"\] \.story-trail-flow/, mode);
}
});
test('all five renderers add one geometry-neutral semantic sigil per primary node', () => {
for (const [mode, example] of Object.entries(CASES)) {
const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', example), 'utf8'));
const expected = source[NODE_COLLECTION[mode]].length;
const staticHtml = render(mode, example, null, 'classic');
const traceHtml = render(mode, example, 'trace', 'classic');
const staticSvg = svgBlock(staticHtml);
const traceSvg = svgBlock(traceHtml);
const sigils = (svg) => [...svg.matchAll(/<g aria-hidden="true" data-semantic-sigil="[^"]+"[\s\S]*?<\/g>/g)].map((match) => match[0]);
assert.equal(sigils(staticSvg).length, expected, mode);
assert.deepEqual(sigils(traceSvg), sigils(staticSvg), `${mode} trace must not change sigil geometry`);
assert.match(staticHtml, /svg \.semantic-sigil \{/i, mode);
assert.match(staticHtml, /svg \.s-database\s+\{ color: var\(--database-stroke\); \}/, mode);
}
});
test('unknown visual presets are rejected by schema validation', () => {
assert.throws(
() => render('architecture', CASES.architecture, null, 'hologram'),
/visual_preset/,
);
});
for (const [mode, example] of Object.entries(CASES)) {
test(`${mode}: trace animation annotates svg, edges, and nodes`, () => {
const svg = svgBlock(render(mode, example));
assert.match(svg, /<svg[^>]+data-animation="trace"/);
assert.match(svg, /data-animate="edge" style="--step:0"/);
assert.match(svg, /data-animate="node" style="--step:0"/);
assert.match(svg, /aria-labelledby="archify-diagram-title archify-diagram-description"/);
assert.match(svg, /<title id="archify-diagram-title">[^<]+<\/title>/);
assert.match(svg, /<desc id="archify-diagram-description">[^<]+<\/desc>/);
assert.match(svg, /id="node-[^"]+" data-node-id="[^"]+"[^>]+role="button"[^>]+aria-pressed="false"/);
assert.match(svg, /data-edge-from="[^"]+" data-edge-to="[^"]+"/);
});
}
test('semantic SVG identity is deterministic for unchanged input', () => {
const first = svgBlock(render('workflow', CASES.workflow));
const second = svgBlock(render('workflow', CASES.workflow));
const hooks = (svg) => [...svg.matchAll(/(?:id="node-|data-edge-from=")[^>]+/g)].map((match) => match[0]);
assert.deepEqual(hooks(first), hooks(second));
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,518 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
ArchitectureDeltaError,
architectureDeltaChangeRows,
canonicalArchitectureJson,
compareArchitecture,
validateArchitectureDeltaHtml,
} from '../delta/architecture-delta.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const cli = path.join(skillRoot, 'bin/archify.mjs');
const baseFixture = path.join(skillRoot, 'examples/checkout-platform.base.architecture.json');
const headFixture = path.join(skillRoot, 'examples/checkout-platform.head.architecture.json');
const checkedArtifact = path.resolve(skillRoot, '../examples/checkout-platform-delta.html');
const checkedReceipt = path.resolve(skillRoot, '../examples/checkout-platform-delta.receipt.json');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-delta-'));
const read = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
const run = (args) => spawnSync(process.execPath, [cli, ...args], { cwd: skillRoot, encoding: 'utf8' });
test('architecture compare classifies authored facts separately from geometry and presentation', () => {
const receipt = compareArchitecture(read(baseFixture), read(headFixture));
assert.equal(receipt.command, 'compare');
assert.equal(receipt.completeness, 'complete');
assert.equal(receipt.proofLevel, 'authored');
assert.deepEqual(receipt.summary.components, {
added: 1,
changed: 1,
evidenceChanged: 0,
removed: 1,
moved: 1,
});
assert.deepEqual(receipt.summary.connections, {
added: 1,
changed: 2,
removed: 1,
rerouted: 1,
});
assert.equal(receipt.summary.presentationChanged, true);
const checkout = receipt.changes.components.find((change) => change.id === 'checkout');
assert.equal(checkout.status, 'changed');
assert.deepEqual(checkout.classifications, ['semantic']);
assert.deepEqual(checkout.changedFields, ['/sublabel']);
const queue = receipt.changes.components.find((change) => change.id === 'queue');
assert.equal(queue.status, 'moved');
assert.deepEqual(queue.classifications, ['geometry']);
assert.deepEqual(queue.changedFields, ['/pos']);
const authorization = receipt.changes.connections.find((change) => change.id === 'authorize-payment');
assert.equal(authorization.status, 'changed');
assert.deepEqual(authorization.classifications, ['geometry', 'topology']);
assert.deepEqual(authorization.changedFields, ['/from', '/fromSide', '/toSide', '/via']);
assert.deepEqual(receipt.changes.connections.find((change) => change.status === 'added').classifications, ['topology']);
const headWithBoundary = read(headFixture);
headWithBoundary.boundaries.push({ kind: 'region', label: 'Fraud edge', wraps: ['fraud'] });
const boundaryReceipt = compareArchitecture(read(baseFixture), headWithBoundary);
assert.deepEqual(boundaryReceipt.changes.boundaries.find((change) => change.label === 'Fraud edge').classifications, ['scope']);
});
test('legend-only changes are presentation changes and never topology changes', () => {
const base = read(baseFixture);
const head = read(baseFixture);
head.meta.legend = {
entries: {
security: { label: 'Trust boundary', visible: true },
database: { visible: false },
},
};
const receipt = compareArchitecture(base, head);
assert.equal(receipt.summary.presentationChanged, true);
assert.deepEqual(receipt.summary.components, {
added: 0,
changed: 0,
evidenceChanged: 0,
removed: 0,
moved: 0,
});
assert.deepEqual(receipt.summary.connections, {
added: 0,
changed: 0,
removed: 0,
rerouted: 0,
});
assert.deepEqual(receipt.changes, { components: [], connections: [], boundaries: [] });
});
test('canonical architecture ignores formatting, entity order, and set-like order', () => {
const original = read(baseFixture);
const reordered = JSON.parse(JSON.stringify(original));
reordered.components.reverse();
reordered.connections.reverse();
reordered.boundaries.reverse();
reordered.boundaries.forEach((boundary) => boundary.wraps.reverse());
assert.equal(canonicalArchitectureJson(reordered), canonicalArchitectureJson(original));
});
test('change navigator order is exact-ID based, complete, unique, and stable', () => {
const receipt = compareArchitecture(read(baseFixture), read(headFixture));
const rows = architectureDeltaChangeRows(receipt);
assert.deepEqual(rows.map((row) => row.key), [
'component:fraud',
'relationship:fraud-check',
'boundary:region:Production region',
'boundary:security-group:Checkout trust zone',
'component:checkout',
'relationship:authorize-payment',
'relationship:persist-order',
'component:queue',
'component:cache',
'relationship:session-read',
'relationship:publish-order',
]);
assert.equal(new Set(rows.map((row) => row.key)).size, rows.length);
assert.equal(rows.length, receipt.changes.components.length + receipt.changes.connections.length + receipt.changes.boundaries.length);
});
test('exact identity fails closed instead of guessing relationships or unrelated systems', () => {
const base = read(baseFixture);
const missingRelationship = read(headFixture);
delete missingRelationship.connections[0].id;
assert.throws(
() => compareArchitecture(base, missingRelationship),
(error) => error instanceof ArchitectureDeltaError
&& error.code === 'delta/relationship-id-required'
&& error.details.paths.includes('/connections/0/id'),
);
const unrelated = read(headFixture);
unrelated.components = unrelated.components.map((component, index) => ({ ...component, id: `other${index}` }));
unrelated.connections = [];
unrelated.boundaries = [];
assert.throws(
() => compareArchitecture(base, unrelated),
(error) => error instanceof ArchitectureDeltaError && error.code === 'delta/no-shared-component-id',
);
});
test('evidence-only component changes keep an enabled exact review contract', () => {
const base = read(baseFixture);
const head = read(baseFixture);
base.components[0].sources = [{ path: 'src/entry.js', line: 1, label: 'baseline' }];
head.components[0].sources = [{ path: 'src/entry.js', line: 2, label: 'head' }];
const receipt = compareArchitecture(base, head);
assert.equal(receipt.changes.components.length, 1);
assert.equal(receipt.changes.components[0].status, 'evidence-changed');
assert.deepEqual(receipt.changes.components[0].classifications, ['evidence']);
const runtime = fs.readFileSync(path.join(skillRoot, 'delta/architecture-delta.mjs'), 'utf8');
assert.match(runtime, /statuses: \['added', 'changed', 'evidence-changed', 'removed', 'moved'\]/);
});
test('mixed semantic and geometry component changes retain both exact forms', () => {
const head = read(headFixture);
head.components.find((component) => component.id === 'queue').sublabel = 'durable queue v2';
const headPath = path.join(tmp, 'mixed-component-head.json');
const output = path.join(tmp, 'mixed-component-delta.html');
fs.writeFileSync(headPath, JSON.stringify(head));
const result = run(['compare', 'architecture', baseFixture, headPath, output, '--json']);
assert.equal(result.status, 0, result.stderr);
const receipt = JSON.parse(result.stdout);
const queue = receipt.changes.components.find((change) => change.id === 'queue');
assert.equal(queue.status, 'changed');
assert.deepEqual(queue.classifications, ['geometry', 'semantic']);
const html = fs.readFileSync(output, 'utf8');
assert.match(html, /data-change-key="component:queue"[^>]+data-change-target-signature="g:changed:geometry,semantic\|g:moved-from:geometry,semantic"/);
assert.deepEqual(validateArchitectureDeltaHtml(html, receipt), { ok: true, checksPassed: 10, checkCount: 10 });
});
test('mixed semantic and geometry relationship changes retain both exact routes', () => {
const head = read(headFixture);
head.connections.find((connection) => connection.id === 'publish-order').label = 'accepted event';
const headPath = path.join(tmp, 'mixed-relationship-head.json');
const output = path.join(tmp, 'mixed-relationship-delta.html');
fs.writeFileSync(headPath, JSON.stringify(head));
const result = run(['compare', 'architecture', baseFixture, headPath, output, '--json']);
assert.equal(result.status, 0, result.stderr);
const receipt = JSON.parse(result.stdout);
const publishOrder = receipt.changes.connections.find((change) => change.id === 'publish-order');
assert.equal(publishOrder.status, 'changed');
assert.deepEqual(publishOrder.classifications, ['geometry', 'semantic']);
const html = fs.readFileSync(output, 'utf8');
assert.match(html, /data-change-key="relationship:publish-order"[^>]+data-change-target-signature="g:changed:geometry,semantic\|g:moved-from:geometry,semantic\|path:changed:geometry,semantic\|path:moved-from:geometry,semantic\|text:changed:\|text:moved-from:"/);
assert.deepEqual(validateArchitectureDeltaHtml(html, receipt), { ok: true, checksPassed: 10, checkCount: 10 });
});
test('baseline boundary title masks stay below current components and carry delta identity', () => {
const documentAt = (pos, pad) => ({
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Boundary mask z-order', quality_profile: 'standard', viewBox: [600, 400] },
components: [{ id: 'node', type: 'backend', label: 'Current node', pos, size: [120, 60] }],
connections: [],
boundaries: [{ kind: 'region', label: 'Boundary label', wraps: ['node'], pad }],
});
const basePath = path.join(tmp, 'boundary-mask.base.json');
const headPath = path.join(tmp, 'boundary-mask.head.json');
const output = path.join(tmp, 'boundary-mask.delta.html');
fs.writeFileSync(basePath, JSON.stringify(documentAt([250, 200], 30)));
fs.writeFileSync(headPath, JSON.stringify(documentAt([224, 180], 40)));
const result = run(['compare', 'architecture', basePath, headPath, output, '--json']);
assert.equal(result.status, 0, result.stderr);
const html = fs.readFileSync(output, 'utf8');
const delta = html.match(/<section class="canvas" data-view="delta">([\s\S]*?)<\/section>/)?.[1] || '';
const currentComponents = delta.indexOf('<!-- Components -->');
const currentNode = delta.indexOf('data-node-id="node"', currentComponents);
const phantomMask = delta.match(
/<rect data-graph-role="structural-frame-label-mask"[^>]*data-delta-state="moved-from"[^>]*data-delta-boundary-state="moved-from"[^>]*data-delta-boundary-mask-key="region:Boundary label"[^>]*\/>/,
)?.[0];
const currentNodeRect = delta.slice(currentNode).match(/<rect\b[^>]*\/>/)?.[0];
assert.ok(phantomMask && currentNodeRect, 'expected the phantom mask and current component rect');
const rect = (tag) => Object.fromEntries(
[...tag.matchAll(/\b(x|y|width|height)="([^"]+)"/g)].map((match) => [match[1], Number(match[2])]),
);
const maskBox = rect(phantomMask);
const nodeBox = rect(currentNodeRect);
const overlaps = maskBox.x < nodeBox.x + nodeBox.width
&& maskBox.x + maskBox.width > nodeBox.x
&& maskBox.y < nodeBox.y + nodeBox.height
&& maskBox.y + maskBox.height > nodeBox.y;
assert.equal(overlaps, true, `expected overlap: ${JSON.stringify({ maskBox, nodeBox })}`);
assert.ok(delta.indexOf(phantomMask) < currentNode, 'phantom mask must paint below the current component');
});
test('same-label node id changes remain one removal plus one addition', () => {
const base = read(baseFixture);
const head = read(baseFixture);
const cache = head.components.find((component) => component.id === 'cache');
cache.id = 'session-store';
head.boundaries.forEach((boundary) => {
boundary.wraps = boundary.wraps.map((id) => (id === 'cache' ? 'session-store' : id));
});
head.connections.find((connection) => connection.id === 'session-read').to = 'session-store';
const receipt = compareArchitecture(base, head);
assert.equal(receipt.changes.components.find((change) => change.id === 'cache').status, 'removed');
assert.equal(receipt.changes.components.find((change) => change.id === 'session-store').status, 'added');
assert.equal(receipt.changes.components.filter((change) => change.headLabel === 'Session Cache' || change.baseLabel === 'Session Cache').length, 2);
});
test('repository mismatch fails and verified matching revisions remain evidence-bounded', () => {
const base = read(baseFixture);
const head = read(headFixture);
base.meta.repository = { url: 'https://github.com/example/one', revision: 'a'.repeat(40) };
head.meta.repository = { url: 'https://github.com/example/two', revision: 'b'.repeat(40) };
assert.throws(
() => compareArchitecture(base, head),
(error) => error instanceof ArchitectureDeltaError && error.code === 'delta/repository-mismatch',
);
head.meta.repository.url = 'https://github.com/EXAMPLE/ONE.git/';
const receipt = compareArchitecture(base, head, { baseVerified: true, headVerified: true });
assert.equal(receipt.proofLevel, 'revision-pinned');
assert.equal(receipt.summary.provenanceChanged, true);
});
test('compare CLI writes a deterministic three-state artifact and complete sidecar receipt', () => {
const first = path.join(tmp, 'first.html');
const second = path.join(tmp, 'second.html');
const result = run(['compare', 'architecture', baseFixture, headFixture, first, '--json']);
assert.equal(result.status, 0, result.stderr);
const repeat = run(['compare', 'architecture', baseFixture, headFixture, second, '--json']);
assert.equal(repeat.status, 0, repeat.stderr);
const firstHtml = fs.readFileSync(first, 'utf8');
const secondHtml = fs.readFileSync(second, 'utf8');
assert.equal(firstHtml, secondHtml);
assert.equal((firstHtml.match(/<section class="canvas" data-view=/g) || []).length, 3);
assert.match(firstHtml, /data-view="delta">/);
assert.match(firstHtml, /data-node-id="cache"[^>]+data-delta-state="removed"/);
assert.match(firstHtml, /data-node-id="fraud"[^>]+data-delta-state="added"/);
assert.match(firstHtml, /data-node-id="queue"[^>]+data-delta-state="moved-from"/);
assert.match(firstHtml, /aria-label="Authored change review"/);
assert.equal((firstHtml.match(/class="change-row"/g) || []).length, 11);
assert.match(firstHtml, /data-change-key="component:fraud"/);
assert.match(firstHtml, /data-change-key="relationship:authorize-payment"/);
assert.match(firstHtml, /data-change-key="boundary:region:Production region"/);
assert.match(firstHtml, /data-change-target-signature="[^"]+"/);
assert.match(firstHtml, /data-delta-boundary-key="region:Production region"/);
assert.equal((firstHtml.match(/class="snapshot-frame"/g) || []).length, 2);
assert.match(firstHtml, /title="Before architecture explorer"/);
assert.match(firstHtml, /title="After architecture explorer"/);
assert.match(firstHtml, /id="export-svg"[^>]*>Export SVG</);
assert.match(firstHtml, /id="share-card"[^>]*>Share Card</);
assert.match(firstHtml, /window\.Archify\.deltaExport = \{ canonicalSvg: canonicalDeltaSvg, shareCard/);
assert.match(firstHtml, /canvas\.width = 1200;[\s\S]*canvas\.height = 630;/);
assert.match(firstHtml, /structural-frame.*stroke:var\(--delta\)!important/);
assert.match(firstHtml, /structural-frame.*data-delta-state="changed".*stroke-dasharray:2 3!important/);
assert.match(firstHtml, /data-delta-boundary-state="added".*fill:#34d399!important/);
assert.match(firstHtml, /delta-boundary-marker\[data-delta-state\]\{color:var\(--delta\)\}/);
assert.match(firstHtml, /No authored architecture changes ·.*movementSummary/);
assert.match(firstHtml, /font-family:"JetBrains Mono",ui-monospace/);
assert.doesNotMatch(firstHtml, /font-family:Inter|body\{min-width:1080px/);
assert.match(firstHtml, /@media\(max-width:760px\)/);
assert.match(firstHtml, /\.canvas svg\{min-width:720px;max-height:none\}/);
assert.match(firstHtml, /\.changes\{overflow-x:auto\}/);
assert.match(firstHtml, /const REVIEW_DWELL_MS = 1400;/);
assert.match(firstHtml, /prefers-reduced-motion: reduce/);
assert.match(firstHtml, /:not\(\[data-delta-review-current\]\)/);
assert.match(firstHtml, /--review-same-opacity:1;--review-change-opacity:1/);
assert.match(firstHtml, /--d-focus:#006b8f/);
assert.match(firstHtml, /document\.querySelectorAll\('#archify-compare-receipt'\)\.length !== 1/);
assert.match(firstHtml, /targetsMatch\(reviewSources\[index\], row, matches\)/);
assert.match(firstHtml, /document\.addEventListener\('visibilitychange'/);
assert.match(firstHtml, /window\.addEventListener\('beforeprint', overview\)/);
assert.match(firstHtml, /aria-current', 'step'/);
assert.match(firstHtml, /event\.key === 'Enter' \|\| event\.key === ' '/);
const deltaShell = firstHtml.replace(/<iframe\b[^>]*><\/iframe>/g, '');
assert.doesNotMatch(deltaShell, /localStorage|sessionStorage|history\.(?:pushState|replaceState)/);
assert.doesNotMatch(deltaShell, /setInterval\(/);
assert.doesNotMatch(deltaShell, /\b(?:SAFE|LOW RISK|MERGEABLE|NO IMPACT|VERIFIED PR)\b/i);
const receipt = JSON.parse(result.stdout);
const sidecar = read(path.join(tmp, 'first.receipt.json'));
assert.deepEqual(sidecar, receipt);
assert.equal(receipt.artifact.sha256, JSON.parse(repeat.stdout).artifact.sha256);
assert.equal(receipt.validation.checksPassed, receipt.validation.checkCount);
assert.equal(receipt.completeness, 'complete');
assert.equal(JSON.stringify(receipt).includes(tmp), false);
assert.deepEqual(validateArchitectureDeltaHtml(firstHtml, receipt), { ok: true, checksPassed: 10, checkCount: 10 });
});
test('checked-in Checkout compare artifact is reproducible from its authoritative inputs', () => {
const artifact = path.join(tmp, 'checked-artifact.html');
const receipt = path.join(tmp, 'checked-artifact.receipt.json');
const result = run([
'compare',
'architecture',
baseFixture,
headFixture,
artifact,
'--receipt',
receipt,
'--quality',
'showcase',
'--json',
]);
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.readFileSync(artifact, 'utf8'), fs.readFileSync(checkedArtifact, 'utf8'));
assert.deepEqual(read(receipt), read(checkedReceipt));
});
test('artifact validation fails closed on missing, duplicate, or self-blessed review identity', () => {
const output = path.join(tmp, 'review-identity.html');
const result = run(['compare', 'architecture', baseFixture, headFixture, output, '--json']);
assert.equal(result.status, 0, result.stderr);
const receipt = JSON.parse(result.stdout);
const html = fs.readFileSync(output, 'utf8');
const deltaSection = html.match(/<section class="canvas" data-view="delta">([\s\S]*?)<\/section>/)?.[1];
assert.ok(deltaSection);
const fraudTag = deltaSection.match(/<g\s+[^>]*\bdata-node-id="fraud"[^>]*>/)?.[0];
assert.ok(fraudTag);
const missing = html.replace(fraudTag, fraudTag.replace('data-node-id="fraud"', 'data-node-id="tampered"'));
assert.throws(
() => validateArchitectureDeltaHtml(missing, receipt),
(error) => error instanceof ArchitectureDeltaError
&& error.code === 'delta/artifact-invalid'
&& error.details.failures.includes('ambiguous Delta identity component:fraud'),
);
const duplicate = html.replace(fraudTag, `${fraudTag}${fraudTag}`);
assert.throws(
() => validateArchitectureDeltaHtml(duplicate, receipt),
(error) => error instanceof ArchitectureDeltaError
&& error.code === 'delta/artifact-invalid'
&& error.details.failures.includes('ambiguous Delta identity component:fraud'),
);
const relationshipGroup = deltaSection.match(/<g\s+[^>]*\bdata-edge-id="fraud-check"[^>]*>[\s\S]*?<\/g>/)?.[0];
assert.ok(relationshipGroup);
const duplicateCompanion = html.replace(relationshipGroup, `${relationshipGroup}${relationshipGroup}`);
assert.throws(
() => validateArchitectureDeltaHtml(duplicateCompanion, receipt),
(error) => error instanceof ArchitectureDeltaError
&& error.code === 'delta/artifact-invalid'
&& error.details.failures.includes('ambiguous Delta target signature relationship:fraud-check'),
);
const duplicateRowTag = duplicateCompanion.match(/<button class="change-row"[^>]*data-change-key="relationship:fraud-check"[^>]*>/)?.[0];
const storedSignature = duplicateRowTag?.match(/data-change-target-signature="([^"]+)"/)?.[1];
assert.ok(duplicateRowTag && storedSignature);
const selfBlessedSignature = [...storedSignature.split('|'), 'g:added:topology'].sort().join('|');
const selfBlessed = duplicateCompanion.replace(
duplicateRowTag,
duplicateRowTag.replace(`data-change-target-signature="${storedSignature}"`, `data-change-target-signature="${selfBlessedSignature}"`),
);
assert.throws(
() => validateArchitectureDeltaHtml(selfBlessed, receipt),
(error) => error instanceof ArchitectureDeltaError
&& error.code === 'delta/artifact-invalid'
&& error.details.failures.includes('ambiguous Delta target signature relationship:fraud-check'),
);
const missingCompanionState = html.replace(
relationshipGroup,
relationshipGroup.replace(/\sdata-delta-state="[^"]+"/, ''),
);
assert.throws(
() => validateArchitectureDeltaHtml(missingCompanionState, receipt),
(error) => error instanceof ArchitectureDeltaError
&& error.code === 'delta/artifact-invalid'
&& error.details.failures.includes('missing Delta target state relationship:fraud-check'),
);
const receiptNode = html.match(/<script id="archify-compare-receipt"[\s\S]*?<\/script>/)?.[0];
assert.ok(receiptNode);
const duplicateReceipt = html.replace(receiptNode, `${receiptNode}${receiptNode}`);
assert.throws(
() => validateArchitectureDeltaHtml(duplicateReceipt, receipt),
(error) => error instanceof ArchitectureDeltaError
&& error.code === 'delta/artifact-invalid'
&& error.details.failures.includes('expected exactly one embedded compare receipt'),
);
const extraDeltaSvg = html.replace(
'<section class="canvas" data-view="delta">',
'<section class="canvas" data-view="delta"><svg viewBox="0 0 1 1"></svg>',
);
assert.throws(
() => validateArchitectureDeltaHtml(extraDeltaSvg, receipt),
(error) => error instanceof ArchitectureDeltaError
&& error.code === 'delta/artifact-invalid'
&& error.details.failures.includes('expected exactly one root SVG in the Delta canvas'),
);
});
test('formatting-only input changes raw proof but not semantic hash or artifact bytes', () => {
const reorderedPath = path.join(tmp, 'reordered-base.json');
const reordered = read(baseFixture);
reordered.components.reverse();
reordered.connections.reverse();
reordered.boundaries.forEach((boundary) => boundary.wraps.reverse());
fs.writeFileSync(reorderedPath, JSON.stringify(reordered, null, 4));
const originalOut = path.join(tmp, 'canonical-original.html');
const reorderedOut = path.join(tmp, 'canonical-reordered.html');
const original = run(['compare', 'architecture', baseFixture, headFixture, originalOut, '--json']);
const changed = run(['compare', 'architecture', reorderedPath, headFixture, reorderedOut, '--json']);
assert.equal(original.status, 0, original.stderr);
assert.equal(changed.status, 0, changed.stderr);
const originalReceipt = JSON.parse(original.stdout);
const changedReceipt = JSON.parse(changed.stdout);
assert.notEqual(originalReceipt.base.rawSha256, changedReceipt.base.rawSha256);
assert.equal(originalReceipt.base.semanticSha256, changedReceipt.base.semanticSha256);
assert.equal(fs.readFileSync(originalOut, 'utf8'), fs.readFileSync(reorderedOut, 'utf8'));
assert.equal(originalReceipt.artifact.sha256, changedReceipt.artifact.sha256);
});
test('compare failure preserves an existing trusted artifact', () => {
const invalid = read(headFixture);
delete invalid.connections[0].id;
const invalidPath = path.join(tmp, 'invalid-head.json');
const output = path.join(tmp, 'preserved.html');
fs.writeFileSync(invalidPath, JSON.stringify(invalid));
fs.writeFileSync(output, 'trusted artifact');
const result = run(['compare', 'architecture', baseFixture, invalidPath, output, '--json']);
assert.notEqual(result.status, 0);
assert.equal(fs.readFileSync(output, 'utf8'), 'trusted artifact');
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.ok, false);
assert.equal(receipt.diagnostics[0].code, 'delta/relationship-id-required');
assert.equal(fs.existsSync(path.join(tmp, 'preserved.receipt.json')), false);
});
test('compare validates raw snapshots before canonicalization can discard invalid fields', () => {
const invalid = read(baseFixture);
invalid.unknown_top_level_fact = true;
const invalidPath = path.join(tmp, 'invalid-raw-base.json');
const output = path.join(tmp, 'invalid-raw-base.html');
fs.writeFileSync(invalidPath, JSON.stringify(invalid));
const result = run(['compare', 'architecture', invalidPath, headFixture, output, '--json']);
assert.notEqual(result.status, 0);
assert.equal(fs.existsSync(output), false);
assert.equal(fs.existsSync(path.join(tmp, 'invalid-raw-base.receipt.json')), false);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.ok, false);
assert.equal(receipt.diagnostics[0].code, 'schema/additionalProperties');
assert.equal(receipt.diagnostics[0].subject.side, 'base');
assert.equal(receipt.diagnostics[0].subject.path, '/');
assert.equal(receipt.diagnostics[0].evidence.additionalProperty, 'unknown_top_level_fact');
});
test('compare commit preflights both targets before replacing a trusted pair', () => {
const caseRoot = fs.mkdtempSync(path.join(tmp, 'pair-target-'));
const output = path.join(caseRoot, 'review.html');
const receiptPath = path.join(caseRoot, 'review.receipt.json');
fs.writeFileSync(output, 'trusted html');
fs.mkdirSync(receiptPath);
const result = run([
'compare', 'architecture', baseFixture, headFixture, output,
'--receipt', receiptPath, '--json',
]);
assert.notEqual(result.status, 0);
assert.equal(fs.readFileSync(output, 'utf8'), 'trusted html');
assert.equal(fs.statSync(receiptPath).isDirectory(), true);
const failure = JSON.parse(result.stdout);
assert.equal(failure.stage, 'commit');
assert.equal(failure.diagnostics[0].code, 'delta/commit-target');
assert.equal(failure.diagnostics[0].evidence.targetType, 'directory');
});
@@ -0,0 +1,95 @@
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 vm from 'node:vm';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const template = fs.readFileSync(path.join(skillRoot, 'assets/template.html'), 'utf8');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-authored-reach-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
function reachabilityFunction() {
const start = template.indexOf('function computeReachability(');
const end = template.indexOf('\n function reachabilityFor(', start);
assert.ok(start >= 0 && end > start, 'template exposes one extractable reachability function');
return vm.runInNewContext(`(${template.slice(start, end)})`);
}
test('authored reachability is available in every typed artifact without entering canonical SVG', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /id="focus-reach" hidden/);
assert.match(html, /id="btn-reach-upstream"[^>]+aria-pressed="false"/);
assert.match(html, /id="btn-reach-downstream"[^>]+aria-pressed="false"/);
assert.match(html, /function computeReachability\(originId, direction, relationships\)/);
assert.match(html, /svg\.setAttribute\('data-reach-active', direction\)/);
assert.doesNotMatch(canonicalSvg(html), /data-reach-(?:active|match|origin|depth)/, mode);
}
});
test('reachability uses stable breadth-first depth, supports cycles, and deduplicates edge fragments', () => {
const compute = reachabilityFunction();
const relationships = [
{ key: 'a-b', from: 'a', to: 'b' },
{ key: 'a-c', from: 'a', to: 'c' },
{ key: 'b-d', from: 'b', to: 'd' },
{ key: 'c-d', from: 'c', to: 'd' },
{ key: 'd-b', from: 'd', to: 'b' },
{ key: 'x-a', from: 'x', to: 'a' },
{ key: 'a-b', from: 'a', to: 'b' },
];
const downstream = compute('a', 'downstream', relationships);
assert.deepEqual(Array.from(downstream.nodeIds), ['a', 'b', 'c', 'd']);
assert.deepEqual({ ...downstream.depths }, { a: 0, b: 1, c: 1, d: 2 });
assert.deepEqual(Array.from(downstream.edgeKeys), ['a-b', 'a-c', 'b-d', 'c-d', 'd-b']);
assert.equal(downstream.maxDepth, 2);
const upstream = compute('d', 'upstream', relationships);
assert.deepEqual(Array.from(upstream.nodeIds), ['d', 'b', 'c', 'a', 'x']);
assert.deepEqual({ ...upstream.depths }, { d: 0, b: 1, c: 1, a: 2, x: 3 });
assert.equal(upstream.maxDepth, 3);
assert.equal(compute('a', 'sideways', relationships), null);
});
test('reachability stays explicit, deep-linkable, keyboard reachable, and export-clean', () => {
assert.match(template, /Authored Reachability is a bounded graph query over the relationships/);
assert.match(template, /direction !== 'upstream' && direction !== 'downstream'/);
assert.match(template, /encodeURIComponent\(activeIds\[0\]\) \+ '&reach=' \+ direction/);
assert.match(template, /applyReachability\(reach, \{ updateUrl: false, toggle: false, reveal: false \}\)/);
assert.match(template, /upstreamBtn\.addEventListener\('click'/);
assert.match(template, /downstreamBtn\.addEventListener\('click'/);
assert.match(template, /clone\.removeAttribute\('data-reach-active'\)/);
assert.match(template, /clone\.querySelectorAll\('\[data-reach-match\], \[data-reach-origin\], \[data-reach-depth\]'/);
assert.match(template, /!clone\.hasAttribute\('data-reach-active'\)/);
assert.match(template, /svg\[data-preset="blueprint"\]\[data-reach-active\]/);
assert.match(template, /\.diagram-container svg\[data-reach-active\] \[data-node-id\]/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,51 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const skill = fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8');
const authoringContract = fs.readFileSync(
path.join(skillRoot, 'references', 'authoring-contract.md'),
'utf8',
);
const schemaReadme = fs.readFileSync(path.join(skillRoot, 'schemas', 'README.md'), 'utf8');
test('semantic relationship labels are preserved and deletion is not a geometry repair', () => {
for (const [name, source] of [['SKILL.md', skill], ['authoring contract', authoringContract]]) {
assert.match(source, /Relationship labels are semantic data/i, name);
assert.match(source, /move the label[\s\S]*adjust the route or spacing[\s\S]*shorten/i, name);
assert.match(source, /protocol[\s\S]*action[\s\S]*direction[\s\S]*synchronous[\s\S]*asynchronous[\s\S]*cross-boundary mechanism/i, name);
assert.match(source, /Omit only wording[\s\S]*fully implied by both endpoints/i, name);
assert.match(source, /Preserve every meaningful label/i, name);
assert.match(source, /deleting it is not\s+a (?:geometry|spacing) repair/i, name);
}
});
test('schema policy documents the workflow v1/v2 compatibility boundary', () => {
assert.match(schemaReadme, /Workflow[^\n]*schema versions? 1 and 2/i);
assert.match(schemaReadme, /other four[^\n]*schema_version[^\n]*1/i);
assert.doesNotMatch(schemaReadme, /schema_version` is `"const": 1`/);
});
test('deployment ownership stays explicit, fact-backed, and cannot be removed to pass', () => {
assert.match(skill, /Omit `meta\.engineering_profile` by default/);
assert.match(skill, /Region.*cluster.*security boundar.*do not.*enable/i);
assert.match(skill, /production deployment topology.*ownership.*fail-closed deployment review/i);
assert.match(skill, /must not remove.*engineering profile.*pass validation/i);
});
test('visual-check stays a pending sidecar receipt instead of a polish claim', () => {
const deliveryContract = fs.readFileSync(
path.join(skillRoot, 'references', 'delivery-contract.md'),
'utf8',
);
for (const [name, source] of [['SKILL.md', skill], ['delivery contract', deliveryContract]]) {
assert.match(source, /visual-check <output\.html> --json/, name);
assert.match(source, /1440×900[\s\S]*1600×1000[\s\S]*1920×1080[\s\S]*2048×1320/, name);
assert.match(source, /visualReview: "pending"/, name);
assert.match(source, /never changes.*delivered|without (?:rerendering or )?modifying/i, name);
}
});
@@ -0,0 +1,376 @@
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, '..');
function render(mode, doc) {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-port-spread-'));
const input = path.join(tmp, 'input.json');
const output = path.join(tmp, 'output.html');
fs.writeFileSync(input, JSON.stringify(doc));
try {
execFileSync('node', [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
input,
output,
], { stdio: ['ignore', 'ignore', 'pipe'] });
return fs.readFileSync(output, 'utf8');
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
}
function connectionPoints(html, id) {
const pattern = new RegExp(`data-edge-id="${id}"[^>]+data-composition-points="([^"]+)"`);
const match = html.match(pattern);
assert.ok(match, `missing rendered connection ${id}`);
return match[1].split(';').map((point) => point.split(',').map(Number));
}
function fanOutArchitecture(connections) {
return {
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Automatic port spread' },
components: [
{ id: 'hub', type: 'backend', label: 'Hub', pos: [100, 280], size: [120, 60] },
{ id: 'upper', type: 'external', label: 'Upper', pos: [500, 100], size: [120, 60] },
{ id: 'middle', type: 'database', label: 'Middle', pos: [500, 280], size: [120, 60] },
{ id: 'lower', type: 'cloud', label: 'Lower', pos: [500, 460], size: [120, 60] },
],
connections,
};
}
test('architecture: automatic fan-out uses distinct symmetric ports with corner clearance', () => {
const html = render('architecture', fanOutArchitecture([
{ id: 'to-upper', from: 'hub', to: 'upper' },
{ id: 'to-middle', from: 'hub', to: 'middle' },
{ id: 'to-lower', from: 'hub', to: 'lower' },
]));
assert.deepEqual(connectionPoints(html, 'to-upper')[0], [220, 296]);
assert.deepEqual(connectionPoints(html, 'to-middle')[0], [220, 310]);
assert.deepEqual(connectionPoints(html, 'to-lower')[0], [220, 324]);
});
test('architecture: automatic port assignment is stable when relationship input order changes', () => {
const connections = [
{ id: 'to-upper', from: 'hub', to: 'upper' },
{ id: 'to-middle', from: 'hub', to: 'middle' },
{ id: 'to-lower', from: 'hub', to: 'lower' },
];
const forward = render('architecture', fanOutArchitecture(connections));
const reversed = render('architecture', fanOutArchitecture([...connections].reverse()));
for (const connection of connections) {
assert.deepEqual(
connectionPoints(forward, connection.id),
connectionPoints(reversed, connection.id),
`${connection.id} moved after input reordering`,
);
}
});
test('architecture: a singly spread near-aligned vertical relationship keeps one direct axis', () => {
const html = render('architecture', {
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Near-aligned fan-out' },
components: [
{ id: 'api', type: 'backend', label: 'API', pos: [675, 300], size: [120, 60] },
{ id: 'auth', type: 'security', label: 'Auth', pos: [570, 120], size: [100, 60] },
{ id: 'cache', type: 'database', label: 'Cache', pos: [685, 120], size: [100, 60] },
],
connections: [
{ id: 'verify', from: 'api', to: 'auth', fromSide: 'top', toSide: 'bottom' },
{ id: 'read', from: 'api', to: 'cache', fromSide: 'top', toSide: 'bottom' },
],
});
assert.deepEqual(connectionPoints(html, 'read'), [[742, 300], [742, 180]]);
assert.notDeepEqual(connectionPoints(html, 'verify')[0], connectionPoints(html, 'read')[0]);
});
test('architecture: a singly spread near-aligned horizontal relationship keeps one direct axis', () => {
const html = render('architecture', {
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Near-aligned horizontal fan-out' },
components: [
{ id: 'hub', type: 'backend', label: 'Hub', pos: [100, 100], size: [120, 60] },
{ id: 'direct', type: 'database', label: 'Direct', pos: [500, 107], size: [120, 60] },
{ id: 'branch', type: 'cloud', label: 'Branch', pos: [500, 300], size: [120, 60] },
],
connections: [
{ id: 'hub-direct', from: 'hub', to: 'direct', fromSide: 'right', toSide: 'left' },
{ id: 'hub-branch', from: 'hub', to: 'branch', fromSide: 'right', toSide: 'left' },
],
});
assert.deepEqual(connectionPoints(html, 'hub-direct'), [[220, 123], [500, 123]]);
assert.notDeepEqual(connectionPoints(html, 'hub-branch')[0], connectionPoints(html, 'hub-direct')[0]);
});
test('architecture: a shared bottom port keeps its aligned child relationship straight', () => {
const doc = {
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Vertical child relationship' },
components: [
{ id: 'parent', type: 'backend', label: 'Parent Session', pos: [300, 100], size: [200, 60] },
{ id: 'terminal', type: 'backend', label: 'Background terminal', pos: [300, 300], size: [200, 60] },
{ id: 'workflow', type: 'backend', label: 'Workflow', pos: [560, 300], size: [180, 60] },
],
connections: [
{ id: 'parent-terminal', from: 'parent', to: 'terminal', fromSide: 'bottom', toSide: 'top' },
{ id: 'parent-workflow', from: 'parent', to: 'workflow', fromSide: 'bottom', toSide: 'top' },
],
};
const forward = render('architecture', doc);
const reversed = render('architecture', {
...doc,
connections: [...doc.connections].reverse(),
});
assert.deepEqual(connectionPoints(forward, 'parent-terminal'), [[393, 160], [393, 300]]);
assert.notDeepEqual(
connectionPoints(forward, 'parent-workflow')[0],
connectionPoints(forward, 'parent-terminal')[0],
);
for (const connection of doc.connections) {
assert.deepEqual(
connectionPoints(forward, connection.id),
connectionPoints(reversed, connection.id),
`${connection.id} moved after input reordering`,
);
}
});
test('architecture: incoming and outgoing relationships keep distinct bottom ports while the direct child stays straight', () => {
const html = render('architecture', {
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Shared incoming and outgoing side' },
components: [
{ id: 'workflow', type: 'backend', label: 'Workflow', pos: [80, 320], size: [180, 60] },
{ id: 'child', type: 'security', label: 'Child boundary', pos: [300, 100], size: [200, 60] },
{ id: 'footer', type: 'frontend', label: 'Footer', pos: [300, 320], size: [200, 60] },
],
connections: [
{ id: 'workflow-child', from: 'workflow', to: 'child', fromSide: 'right', toSide: 'bottom' },
{ id: 'child-footer', from: 'child', to: 'footer', fromSide: 'bottom', toSide: 'top' },
],
});
assert.deepEqual(connectionPoints(html, 'child-footer'), [[407, 160], [407, 320]]);
assert.notDeepEqual(
connectionPoints(html, 'workflow-child').at(-1),
connectionPoints(html, 'child-footer')[0],
);
});
test('architecture: a near-aligned relationship keeps the outside bridge when both endpoints are spread', () => {
const html = render('architecture', {
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Two-sided port competition' },
components: [
{ id: 'source', type: 'backend', label: 'Source', pos: [300, 320], size: [160, 60] },
{ id: 'source-peer', type: 'backend', label: 'Source peer', pos: [80, 320], size: [160, 60] },
{ id: 'target', type: 'database', label: 'Target', pos: [300, 100], size: [160, 60] },
{ id: 'target-peer', type: 'database', label: 'Target peer', pos: [560, 100], size: [160, 60] },
],
connections: [
{ id: 'source-target', from: 'source', to: 'target', fromSide: 'top', toSide: 'bottom' },
{ id: 'source-peer-target', from: 'source-peer', to: 'target', fromSide: 'top', toSide: 'bottom' },
{ id: 'source-target-peer', from: 'source', to: 'target-peer', fromSide: 'top', toSide: 'bottom' },
],
});
const points = connectionPoints(html, 'source-target');
assert.ok(points.length > 2);
assert.notEqual(points[0][0], points.at(-1)[0]);
});
test('architecture: a singly spread near-aligned relationship keeps the bridge when its direct axis is blocked', () => {
const html = render('architecture', {
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Blocked vertical axis' },
components: [
{ id: 'parent', type: 'backend', label: 'Parent', pos: [300, 100], size: [200, 60] },
{ id: 'terminal', type: 'backend', label: 'Terminal', pos: [300, 400], size: [200, 60] },
{ id: 'workflow', type: 'backend', label: 'Workflow', pos: [560, 400], size: [180, 60] },
{ id: 'obstacle', type: 'external', label: 'X', pos: [382, 245], size: [26, 60] },
],
connections: [
{ id: 'parent-terminal', from: 'parent', to: 'terminal', fromSide: 'bottom', toSide: 'top' },
{ id: 'parent-workflow', from: 'parent', to: 'workflow', fromSide: 'bottom', toSide: 'top' },
],
});
const points = connectionPoints(html, 'parent-terminal');
assert.ok(points.length > 2);
assert.notEqual(points[0][0], points.at(-1)[0]);
});
test('architecture: single and explicitly positioned relationships keep legacy anchors', () => {
const doc = fanOutArchitecture([
{ id: 'single', from: 'hub', to: 'middle' },
{ id: 'via', from: 'hub', to: 'upper', via: [[300, 310], [300, 130]] },
{ id: 'fixed-route', from: 'hub', to: 'lower', route: 'orthogonal-h' },
{ id: 'fixed-label', from: 'hub', to: 'upper', label: 'contract', labelAt: [360, 200] },
]);
const html = render('architecture', doc);
assert.deepEqual(connectionPoints(html, 'single'), [[220, 310], [500, 310]]);
assert.deepEqual(connectionPoints(html, 'via'), [[220, 310], [300, 310], [300, 130], [500, 130]]);
assert.deepEqual(connectionPoints(html, 'fixed-route'), [[220, 310], [360, 310], [360, 490], [500, 490]]);
assert.deepEqual(connectionPoints(html, 'fixed-label'), [[220, 310], [360, 310], [360, 130], [500, 130]]);
});
test('architecture: an unspread near-aligned connection shares one horizontal axis', () => {
const html = render('architecture', {
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Near-aligned single connection' },
components: [
{ id: 'console', type: 'frontend', label: 'Console', pos: [260, 300], size: [170, 64] },
{ id: 'controlplane', type: 'backend', label: 'Control plane', pos: [500, 300], size: [190, 72] },
],
connections: [
{ id: 'console-controlplane', from: 'console', to: 'controlplane', label: 'REST /api', variant: 'emphasis', labelDy: -36 },
],
});
assert.deepEqual(connectionPoints(html, 'console-controlplane'), [[430, 332], [500, 332]]);
});
test('workflow: automatic cross-lane fan-out selects distinct perpendicular source sides', () => {
const html = render('workflow', {
schema_version: 1,
diagram_type: 'workflow',
meta: { title: 'Workflow port spread' },
lanes: [
{ id: 'upper-lane', label: 'Upper' },
{ id: 'hub-lane', label: 'Hub' },
{ id: 'lower-lane', label: 'Lower' },
],
nodes: [
{ id: 'hub', lane: 'hub-lane', col: 0, type: 'backend', label: 'Hub' },
{ id: 'upper', lane: 'upper-lane', col: 3, type: 'external', label: 'Upper' },
{ id: 'middle', lane: 'hub-lane', col: 3, type: 'database', label: 'Middle' },
{ id: 'lower', lane: 'lower-lane', col: 3, type: 'cloud', label: 'Lower' },
],
edges: [
{ id: 'to-upper', from: 'hub', to: 'upper' },
{ id: 'to-middle', from: 'hub', to: 'middle' },
{ id: 'to-lower', from: 'hub', to: 'lower' },
],
});
assert.deepEqual(connectionPoints(html, 'to-upper')[0], [88, 217]);
assert.deepEqual(connectionPoints(html, 'to-middle')[0], [134, 243]);
assert.deepEqual(connectionPoints(html, 'to-lower')[0], [88, 269]);
});
test('dataflow: automatic fan-out spreads flows without changing their authored topology', () => {
const html = render('dataflow', {
schema_version: 1,
diagram_type: 'dataflow',
meta: { title: 'Data-flow port spread' },
stages: [{ label: 'Source' }, { label: 'Transform' }, { label: 'Sinks' }],
nodes: [
{ id: 'hub', type: 'backend', label: 'Hub', stage: 0, row: 2 },
{ id: 'upper', type: 'external', label: 'Upper', stage: 2, row: 0 },
{ id: 'middle', type: 'database', label: 'Middle', stage: 2, row: 2 },
{ id: 'lower', type: 'cloud', label: 'Lower', stage: 2, row: 4 },
],
flows: [
{ id: 'to-upper', from: 'hub', to: 'upper', label: 'upper feed' },
{ id: 'to-middle', from: 'hub', to: 'middle', label: 'middle feed' },
{ id: 'to-lower', from: 'hub', to: 'lower', label: 'lower feed' },
],
});
assert.deepEqual(connectionPoints(html, 'to-upper')[0], [156, 372]);
assert.deepEqual(connectionPoints(html, 'to-middle')[0], [156, 385]);
assert.deepEqual(connectionPoints(html, 'to-lower')[0], [156, 398]);
});
test('lifecycle: automatic fan-out spreads transitions across lifecycle bands', () => {
const html = render('lifecycle', {
schema_version: 1,
diagram_type: 'lifecycle',
meta: { title: 'Lifecycle port spread' },
lanes: [
{ id: 'main', label: 'Main' },
{ id: 'event', label: 'Events' },
{ id: 'terminal', label: 'Outcomes' },
],
states: [
{ id: 'hub', type: 'active', label: 'Hub', lane: 'event', col: 0 },
{ id: 'upper', type: 'waiting', label: 'Upper', lane: 'main', col: 4 },
{ id: 'middle', type: 'success', label: 'Middle', lane: 'event', col: 2 },
{ id: 'lower', type: 'failure', label: 'Lower', lane: 'terminal', col: 2 },
],
transitions: [
{ id: 'to-upper', from: 'hub', to: 'upper' },
{ id: 'to-middle', from: 'hub', to: 'middle' },
{ id: 'to-lower', from: 'hub', to: 'lower' },
],
});
assert.deepEqual(connectionPoints(html, 'to-upper')[0], [465, 294]);
assert.deepEqual(connectionPoints(html, 'to-middle')[0], [465, 307]);
assert.deepEqual(connectionPoints(html, 'to-lower')[0], [465, 320]);
});
test('lifecycle: same-band port spread remains orthogonal', () => {
const html = render('lifecycle', {
schema_version: 1,
diagram_type: 'lifecycle',
meta: { title: 'Orthogonal same-band spread' },
lanes: [{ id: 'main', label: 'Main' }],
states: [
{ id: 'hub', type: 'active', label: 'Hub', lane: 'main', col: 0 },
{ id: 'upper', type: 'waiting', label: 'Upper', lane: 'main', col: 2, yOffset: -50 },
{ id: 'lower', type: 'success', label: 'Lower', lane: 'main', col: 4, yOffset: 50 },
],
transitions: [
{ id: 'to-upper', from: 'hub', to: 'upper' },
{ id: 'to-lower', from: 'hub', to: 'lower' },
],
});
assert.deepEqual(connectionPoints(html, 'to-upper'), [
[153, 150], [248, 150], [248, 107], [343, 107],
]);
assert.deepEqual(connectionPoints(html, 'to-lower'), [
[153, 164], [402, 164], [402, 207], [651, 207],
]);
});
test('skill and READMEs describe automatic port spread as bounded default behavior', () => {
const skill = fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8');
assert.match(skill, /Automatic Port Spread is a default renderer behavior/);
assert.match(skill, /single relationship|single relationships/);
assert.match(skill, /explicit `via`.*`channelX`.*`channelY`.*`labelAt`/);
assert.match(skill, /facing automatic ports \(`left`\/`right` or `top`\/`bottom`\).*one shared axis/);
const authoringContract = fs.readFileSync(path.join(skillRoot, 'references/authoring-contract.md'), 'utf8');
assert.match(authoringContract, /unobstructed facing ports.*may share one horizontal or vertical axis/);
const repoRoot = path.resolve(skillRoot, '..');
for (const file of ['README.md', 'README_EN.md']) {
assert.match(fs.readFileSync(path.join(repoRoot, file), 'utf8'), /shared automatic endpoints spread deterministically/);
}
assert.match(fs.readFileSync(path.join(repoRoot, 'README_ZH.md'), 'utf8'), /共享的自动端点会确定性展开/);
});
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(here, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-base-input-compatibility-'));
function renderBaseFixture(type, name) {
const input = path.join(skillRoot, 'test', 'fixtures', 'v1-baseline', name);
const output = path.join(tmp, `${name}.html`);
return spawnSync(process.execPath, [
path.join(skillRoot, 'bin', 'archify.mjs'),
'render',
type,
input,
output,
], {
cwd: skillRoot,
encoding: 'utf8',
});
}
test('base named-route fixtures remain valid without redundant authored endpoint sides', () => {
for (const [type, name] of [
['dataflow', 'event-stream.dataflow.json'],
['architecture', 'production-deployment.architecture.json'],
]) {
const result = renderBaseFixture(type, name);
assert.equal(
result.status,
0,
`${type} base input must remain valid:\n${result.stdout || result.stderr}`,
);
}
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,654 @@
import assert from 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { BRAND_MARKS } from '../renderers/shared/generated-brand-marks.mjs';
import { isPrivateBrandAddress, prepareDiagramBrandMarks } from '../renderers/shared/brand-marks.mjs';
const here = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(here, '..');
const cli = path.join(skillRoot, 'bin', 'archify.mjs');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-brand-marks-'));
const cases = {
architecture: ['web-app.architecture.json', 'components'],
workflow: ['agent-tool-call.workflow.json', 'nodes'],
sequence: ['cache-miss-request.sequence.json', 'participants'],
dataflow: ['product-analytics.dataflow.json', 'nodes'],
lifecycle: ['agent-run.lifecycle.json', 'states'],
};
function writeFixture(type, name, brand, customize) {
const [example, collection] = cases[type];
const value = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', example), 'utf8'));
value[collection][0].brand = brand;
customize?.(value, value[collection][0]);
const file = path.join(tmp, `${name}.${type}.json`);
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
return file;
}
function renderSync(type, input, name, env = {}) {
const output = path.join(tmp, `${name}.html`);
const result = spawnSync(process.execPath, [
path.join(skillRoot, `renderers/${type}/render-${type}.mjs`),
input,
output,
], {
cwd: skillRoot,
encoding: 'utf8',
env: { ...process.env, ...env },
});
return { result, output, html: fs.existsSync(output) ? fs.readFileSync(output, 'utf8') : '' };
}
function renderAsync(type, input, name, env = {}) {
const output = path.join(tmp, `${name}.html`);
return new Promise((resolve) => {
const child = spawn(process.execPath, [
path.join(skillRoot, `renderers/${type}/render-${type}.mjs`),
input,
output,
], {
cwd: skillRoot,
env: { ...process.env, ...env },
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.on('close', (status) => resolve({
status,
stdout,
stderr,
output,
html: fs.existsSync(output) ? fs.readFileSync(output, 'utf8') : '',
}));
});
}
function runCliAsync(args, env = {}) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [cli, ...args], {
cwd: skillRoot,
env: { ...process.env, ...env },
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.on('close', (status) => resolve({ status, stdout, stderr }));
});
}
function nodeBlock(html, id) {
const startToken = `<g id="node-${id}"`;
const start = html.indexOf(startToken);
if (start === -1) return '';
const candidates = [
html.indexOf('\n <g id="node-', start + startToken.length),
html.indexOf('\n <!-- Connection labels', start + startToken.length),
html.indexOf('\n <!-- Transition labels', start + startToken.length),
html.indexOf('\n <!-- Message labels', start + startToken.length),
].filter((value) => value !== -1);
return html.slice(start, candidates.length ? Math.min(...candidates) : html.length);
}
test('generated catalog exposes a substantial, unique, provenance-backed preset library', () => {
assert.equal(BRAND_MARKS.length, 107);
assert.equal(new Set(BRAND_MARKS.map((mark) => mark.id)).size, BRAND_MARKS.length);
for (const mark of BRAND_MARKS) {
assert.match(mark.id, /^[a-z0-9]+(?:-[a-z0-9]+)*$/);
assert.ok(mark.title);
assert.ok(mark.category);
assert.match(mark.hex, /^[0-9A-F]{6}$/i);
assert.match(mark.path, /^[Mm]/);
assert.ok(mark.provenance?.source);
}
});
test('brand discovery resolves model names, aliases, domains, and Chinese channel aliases', () => {
for (const [query, expected] of [
['GPT', 'openai'],
['Gemini', 'google-gemini'],
['github.com', 'github'],
['微信', 'wechat'],
]) {
const result = spawnSync(process.execPath, [cli, 'brands', query, '--json'], {
cwd: skillRoot,
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.ok, true);
assert.ok(receipt.marks.some((mark) => mark.id === expected), query);
}
});
test('all five renderers keep the semantic sigil and add one export-safe brand badge', () => {
for (const type of Object.keys(cases)) {
const input = writeFixture(type, `preset-${type}`, 'openai', (_diagram, node) => {
if (type === 'lifecycle') node.step = node.step || '01';
});
const { result, html } = renderSync(type, input, `preset-${type}`);
assert.equal(result.status, 0, `${type}: ${result.stderr || result.stdout}`);
assert.match(html, /data-node-brand="OpenAI"/i, type);
assert.match(html, /data-brand-mark="openai"[^>]+data-brand-status="preset"/i, type);
assert.match(html, /class="semantic-sigil /, type);
assert.match(html, /<title>[^<]*OpenAI<\/title>/i, type);
const [, collection] = cases[type];
const diagram = JSON.parse(fs.readFileSync(input, 'utf8'));
const block = nodeBlock(html, diagram[collection][0].id);
const frame = block.match(/<rect x="([-\d.]+)" y="([-\d.]+)" width="([-\d.]+)" height="([-\d.]+)" rx="[^"]+" class="c-mask"\/>/);
const semantic = block.match(/data-semantic-sigil[^>]+translate\(([-\d.]+) ([-\d.]+)\)/);
const brand = block.match(/data-brand-mark="openai"[^>]+translate\(([-\d.]+) ([-\d.]+)\)">\s*<rect width="([-\d.]+)" height="([-\d.]+)" rx="([-\d.]+)" class="brand-mark-badge"\/>/);
assert.ok(frame && semantic && brand, `${type}: expected node frame, semantic sigil, and brand badge`);
const [frameX, frameY, frameWidth] = frame.slice(1, 4).map(Number);
const [, semanticY] = semantic.slice(1, 3).map(Number);
const [brandX, brandY, brandWidth, brandHeight, brandRadius] = brand.slice(1, 6).map(Number);
assert.equal(brandWidth, 16, `${type}: brand badge width`);
assert.equal(brandHeight, 16, `${type}: brand badge height`);
assert.equal(brandRadius, 4, `${type}: brand badge radius`);
assert.equal(brandY - frameY, 6, `${type}: brand badge top inset`);
assert.equal(frameX + frameWidth - (brandX + brandWidth), 6, `${type}: brand badge right inset`);
assert.equal(brandY, semanticY, `${type}: brand and semantic marks share a top rail`);
}
});
test('a branded node fails before its semantic sigil, label, and brand badge can overlap', () => {
const input = writeFixture('workflow', 'narrow-brand-rail', 'openai', (_diagram, node) => {
node.label = 'A';
delete node.sublabel;
node.width = 32;
});
const { result, html } = renderSync('workflow', input, 'narrow-brand-rail');
assert.equal(result.status, 1, result.stderr || result.stdout);
assert.match(result.stderr, /brand top rail/i);
assert.equal(html, '');
});
test('every renderer enforces the same collision-free brand top rail', () => {
for (const type of ['architecture', 'sequence', 'dataflow', 'lifecycle']) {
const input = writeFixture(type, `narrow-brand-rail-${type}`, 'openai', (_diagram, node) => {
node.label = type === 'sequence' ? 'ABCDEFGHI' : 'A';
delete node.sublabel;
delete node.tag;
if (type === 'architecture') node.size = [32, 60];
if (type === 'dataflow' || type === 'lifecycle') node.width = 48;
});
const { result, html } = renderSync(type, input, `narrow-brand-rail-${type}`);
assert.equal(result.status, 1, `${type}: ${result.stderr || result.stdout}`);
assert.match(result.stderr, /brand top rail/i, type);
assert.equal(html, '', type);
}
});
test('branded lifecycle states move the semantic stamp left and keep the brand at upper right', () => {
const input = writeFixture('lifecycle', 'lifecycle-placement', 'openai', (_diagram, node) => {
node.step = '01';
});
const { result, html } = renderSync('lifecycle', input, 'lifecycle-placement');
assert.equal(result.status, 0, result.stderr || result.stdout);
const id = JSON.parse(fs.readFileSync(input, 'utf8')).states[0].id;
const block = nodeBlock(html, id);
const semanticX = Number(block.match(/data-semantic-sigil[^>]+translate\(([-\d.]+)/)?.[1]);
const brandX = Number(block.match(/data-brand-mark[^>]+translate\(([-\d.]+)/)?.[1]);
assert.ok(Number.isFinite(semanticX) && Number.isFinite(brandX) && semanticX < brandX, block);
assert.match(block, /data-detail="fine"[^>]+>01<\/text>/);
});
test('known-brand URLs use the bundled vector instead of the network', () => {
const input = writeFixture('architecture', 'known-domain', 'https://github.com/tt-a1i/archify');
const { result, html } = renderSync('architecture', input, 'known-domain');
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(html, /data-brand-mark="github"[^>]+data-brand-status="preset"/);
assert.doesNotMatch(html, /data-brand-status="captured"/);
});
test('unknown URL strings fail closed until an exact captured digest is authored', () => {
const input = writeFixture('architecture', 'unpinned-link', 'https://brand.example.invalid/');
const result = spawnSync(process.execPath, [cli, 'validate', 'architecture', input, '--json'], {
cwd: skillRoot,
encoding: 'utf8',
});
assert.equal(result.status, 1, result.stderr || result.stdout);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.ok, false);
assert.ok(receipt.diagnostics.some((entry) => entry.code === 'brand/unpinned-url'));
assert.ok(receipt.diagnostics.some((entry) => entry.supportedFixes.some((fix) => fix.includes('brands capture'))));
});
test('capture command returns a digest-pinned brand object that renders reproducibly', async () => {
let pageHits = 0;
let iconHits = 0;
const icon = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64');
const server = http.createServer((request, response) => {
if (request.url === '/mark.png') {
iconHits += 1;
response.writeHead(200, { 'content-type': 'image/png' });
response.end(icon);
return;
}
pageHits += 1;
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
response.end('<!doctype html><title>Example Studio</title><link rel="icon" type="image/png" href="/mark.png"><h1>Example Studio</h1>');
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const address = server.address();
const url = `http://127.0.0.1:${address.port}/studio`;
const capture = await runCliAsync(['brands', 'capture', url, '--json'], { ARCHIFY_BRAND_ALLOW_PRIVATE: '1' });
assert.equal(capture.status, 0, capture.stderr || capture.stdout);
const receipt = JSON.parse(capture.stdout);
assert.equal(receipt.ok, true);
assert.deepEqual(receipt.brand, {
url,
sha256: createHash('sha256').update(icon).digest('hex'),
});
const input = writeFixture('architecture', 'captured-link', receipt.brand);
const rendered = await renderAsync('architecture', input, 'captured-link', { ARCHIFY_BRAND_ALLOW_PRIVATE: '1' });
assert.equal(rendered.status, 0, rendered.stderr || rendered.stdout);
assert.equal(pageHits, 2);
assert.equal(iconHits, 2);
assert.match(rendered.html, /data-brand-status="captured"/);
assert.match(rendered.html, /data:image\/png;base64,/);
assert.match(rendered.html, new RegExp(`data-brand-sha256="${receipt.brand.sha256}"`));
assert.ok(!rendered.html.includes('http://127.0.0.1') || rendered.html.includes('data-node-brand-source='));
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test('a pinned brand fails closed when the remote icon digest changes', async () => {
const firstIcon = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64');
const changedIcon = Buffer.from(firstIcon);
changedIcon[45] ^= 1;
let iconHits = 0;
const server = http.createServer((request, response) => {
if (request.url === '/mark.png') {
iconHits += 1;
response.writeHead(200, { 'content-type': 'image/png' });
response.end(iconHits === 1 ? firstIcon : changedIcon);
return;
}
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
response.end('<!doctype html><title>Changing site</title><link rel="icon" type="image/png" href="/mark.png">');
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const address = server.address();
const url = `http://127.0.0.1:${address.port}/`;
const capture = await runCliAsync(['brands', 'capture', url, '--json'], { ARCHIFY_BRAND_ALLOW_PRIVATE: '1' });
assert.equal(capture.status, 0, capture.stderr || capture.stdout);
const brand = JSON.parse(capture.stdout).brand;
const input = writeFixture('architecture', 'changed-digest', brand);
const result = await runCliAsync(['validate', 'architecture', input, '--json'], { ARCHIFY_BRAND_ALLOW_PRIVATE: '1' });
assert.equal(result.status, 1, result.stderr || result.stdout);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.diagnostics.filter((entry) => entry.code === 'brand/digest-mismatch').length, 1);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test('a pinned brand keeps identical artifact metadata when the remote page title changes', async () => {
const icon = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64');
let pageHits = 0;
const server = http.createServer((request, response) => {
if (request.url === '/mark.png') {
response.writeHead(200, { 'content-type': 'image/png' });
response.end(icon);
return;
}
pageHits += 1;
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
response.end(`<!doctype html><title>Title ${pageHits}</title><link rel="icon" type="image/png" href="/mark.png">`);
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const address = server.address();
const url = `http://127.0.0.1:${address.port}/`;
const capture = await runCliAsync(['brands', 'capture', url, '--json'], { ARCHIFY_BRAND_ALLOW_PRIVATE: '1' });
assert.equal(capture.status, 0, capture.stderr || capture.stdout);
const input = writeFixture('architecture', 'stable-title', JSON.parse(capture.stdout).brand);
const first = await renderAsync('architecture', input, 'stable-title-first', { ARCHIFY_BRAND_ALLOW_PRIVATE: '1' });
const second = await renderAsync('architecture', input, 'stable-title-second', { ARCHIFY_BRAND_ALLOW_PRIVATE: '1' });
assert.equal(first.status, 0, first.stderr || first.stdout);
assert.equal(second.status, 0, second.stderr || second.stdout);
assert.equal(first.html, second.html);
assert.match(first.html, /data-brand-title="127\.0\.0\.1"/);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test('each prepare call rechecks pinned remote bytes instead of trusting a process-wide cache', async () => {
const firstIcon = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64');
const changedIcon = Buffer.from(firstIcon);
changedIcon[45] ^= 1;
let iconHits = 0;
const server = http.createServer((request, response) => {
if (request.url === '/mark.png') {
iconHits += 1;
response.writeHead(200, { 'content-type': 'image/png' });
response.end(iconHits === 1 ? firstIcon : changedIcon);
return;
}
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
response.end('<!doctype html><title>Changing site</title><link rel="icon" type="image/png" href="/mark.png">');
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const priorAllowPrivate = process.env.ARCHIFY_BRAND_ALLOW_PRIVATE;
process.env.ARCHIFY_BRAND_ALLOW_PRIVATE = '1';
try {
const address = server.address();
const diagram = {
components: [{
id: 'remote',
label: 'Remote',
brand: {
url: `http://127.0.0.1:${address.port}/`,
sha256: createHash('sha256').update(firstIcon).digest('hex'),
},
}],
};
await prepareDiagramBrandMarks('architecture', diagram);
await assert.rejects(
prepareDiagramBrandMarks('architecture', diagram),
/brand digest changed/i,
);
assert.equal(iconHits, 2);
} finally {
if (priorAllowPrivate === undefined) delete process.env.ARCHIFY_BRAND_ALLOW_PRIVATE;
else process.env.ARCHIFY_BRAND_ALLOW_PRIVATE = priorAllowPrivate;
await new Promise((resolve) => server.close(resolve));
}
});
test('capture blocks IPv4-mapped IPv6 loopback and metadata destinations before connecting', async () => {
for (const url of [
'http://[::ffff:127.0.0.1]/',
'http://[::ffff:169.254.169.254]/',
'http://[::192.168.1.1]/',
'http://[64:ff9b::c0a8:101]/',
'http://[2002:c0a8:0101::]/',
'http://[ff02::1]/',
'http://192.0.2.1/',
'http://198.51.100.1/',
'http://203.0.113.1/',
]) {
const capture = await runCliAsync(['brands', 'capture', url, '--json']);
assert.notEqual(capture.status, 0, `${url}: ${capture.stderr || capture.stdout}`);
assert.match(capture.stderr, /private brand links are not fetched/i, url);
}
});
test('address classification blocks exact reserved ranges without rejecting adjacent public IPv4 space', () => {
for (const address of ['192.0.2.1', '192.88.99.1', '198.51.100.1', '203.0.113.1']) {
assert.equal(isPrivateBrandAddress(address), true, address);
}
for (const address of ['192.2.1.1', '192.88.98.1', '198.51.99.1', '203.0.112.1']) {
assert.equal(isPrivateBrandAddress(address), false, address);
}
});
test('capture requires the standard port for the selected web protocol', async () => {
for (const url of [
'http://brand.example.invalid:443/',
'https://brand.example.invalid:80/',
'https://github.com:80/',
]) {
const capture = await runCliAsync(['brands', 'capture', url, '--json']);
assert.notEqual(capture.status, 0, capture.stdout);
assert.match(capture.stderr, /standard web port/i, url);
}
});
test('capture rejects credentials even when the URL domain matches a bundled preset', async () => {
const capture = await runCliAsync(['brands', 'capture', 'https://user:secret@github.com/', '--json']);
assert.notEqual(capture.status, 0, capture.stdout);
assert.match(capture.stderr, /cannot contain credentials/i);
});
test('rendering many pinned brands limits concurrent remote capture work', async () => {
const icon = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64');
const sha256 = createHash('sha256').update(icon).digest('hex');
let active = 0;
let maximumActive = 0;
const server = http.createServer((request, response) => {
active += 1;
maximumActive = Math.max(maximumActive, active);
setTimeout(() => {
if (request.url.endsWith('.png')) {
response.writeHead(200, { 'content-type': 'image/png' });
active -= 1;
response.end(icon);
} else {
const suffix = request.url.replace(/^\/site-/, '');
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
active -= 1;
response.end(`<!doctype html><title>Site ${suffix}</title><link rel="icon" type="image/png" href="/mark-${suffix}.png">`);
}
}, 40);
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const address = server.address();
const input = writeFixture('architecture', 'bounded-capture', 'openai', (diagram) => {
diagram.components.forEach((node, index) => {
node.brand = {
url: `http://127.0.0.1:${address.port}/site-${index}`,
sha256,
};
});
});
const rendered = await renderAsync('architecture', input, 'bounded-capture', { ARCHIFY_BRAND_ALLOW_PRIVATE: '1' });
assert.equal(rendered.status, 0, rendered.stderr || rendered.stdout);
assert.ok(maximumActive <= 3, `expected at most 3 concurrent requests, observed ${maximumActive}`);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test('rendering many pinned brands shares one diagram capture deadline', async () => {
const icon = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64');
const sha256 = createHash('sha256').update(icon).digest('hex');
const server = http.createServer((_request, response) => {
setTimeout(() => {
response.writeHead(200, { 'content-type': 'image/png' });
response.end(icon);
}, 80);
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const address = server.address();
const input = writeFixture('architecture', 'diagram-deadline', 'openai', (diagram) => {
diagram.components.forEach((node, index) => {
node.brand = {
url: `http://127.0.0.1:${address.port}/mark-${index}.png`,
sha256,
};
});
});
const rendered = await renderAsync('architecture', input, 'diagram-deadline', {
ARCHIFY_BRAND_ALLOW_PRIVATE: '1',
ARCHIFY_BRAND_CAPTURE_TIMEOUT_MS: '100',
});
assert.notEqual(rendered.status, 0, rendered.stdout);
assert.match(rendered.stderr, /abort|timed? ?out|timeout/i);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test('capture applies one total deadline across the page and icon requests', async () => {
const icon = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64');
const server = http.createServer((request, response) => {
setTimeout(() => {
if (request.url === '/mark.png') {
response.writeHead(200, { 'content-type': 'image/png' });
response.end(icon);
} else {
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
response.end('<!doctype html><title>Slow site</title><link rel="icon" type="image/png" href="/mark.png">');
}
}, 100);
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const address = server.address();
const capture = await runCliAsync(
['brands', 'capture', `http://127.0.0.1:${address.port}/`, '--json'],
{
ARCHIFY_BRAND_ALLOW_PRIVATE: '1',
ARCHIFY_BRAND_CAPTURE_TIMEOUT_MS: '150',
},
);
assert.notEqual(capture.status, 0, capture.stdout);
assert.match(capture.stderr, /abort|timed? ?out|timeout/i);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test('capture rejects remote SVG even when the document appears passive', async () => {
const server = http.createServer((request, response) => {
if (request.url === '/mark.svg') {
response.writeHead(200, { 'content-type': 'image/svg+xml' });
response.end('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24"/></svg>');
return;
}
if (request.url === '/favicon.ico') {
response.writeHead(404);
response.end();
return;
}
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
response.end('<!doctype html><title>SVG mark</title><link rel="icon" type="image/svg+xml" href="/mark.svg">');
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const address = server.address();
const capture = await runCliAsync(
['brands', 'capture', `http://127.0.0.1:${address.port}/studio`, '--json'],
{ ARCHIFY_BRAND_ALLOW_PRIVATE: '1' },
);
assert.notEqual(capture.status, 0, capture.stdout);
assert.match(capture.stderr, /unsupported brand image type image\/svg\+xml/i);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test('unsupported SVG declarations cannot crowd out the favicon fallback', async () => {
const icon = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64');
let fallbackHits = 0;
const server = http.createServer((request, response) => {
if (request.url === '/favicon.ico') {
fallbackHits += 1;
response.writeHead(200, { 'content-type': 'image/png' });
response.end(icon);
return;
}
if (request.url?.endsWith('.svg')) {
response.writeHead(200, { 'content-type': 'image/svg+xml' });
response.end('<svg xmlns="http://www.w3.org/2000/svg"/>');
return;
}
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
response.end(`<!doctype html><title>Fallback mark</title>${Array.from(
{ length: 6 },
(_, index) => `<link rel="icon" type="image/svg+xml" href="/mark-${index}.svg">`,
).join('')}`);
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const address = server.address();
const capture = await runCliAsync(
['brands', 'capture', `http://127.0.0.1:${address.port}/`, '--json'],
{ ARCHIFY_BRAND_ALLOW_PRIVATE: '1' },
);
assert.equal(capture.status, 0, capture.stderr || capture.stdout);
const receipt = JSON.parse(capture.stdout);
assert.equal(receipt.evidence.contentType, 'image/png');
assert.equal(fallbackHits, 1);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test('capture rejects an image whose bytes do not match its declared media type', async () => {
const server = http.createServer((_request, response) => {
response.writeHead(200, { 'content-type': 'image/png' });
response.end('<html>not a png</html>');
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const address = server.address();
const capture = await runCliAsync(
['brands', 'capture', `http://127.0.0.1:${address.port}/mark.png`, '--json'],
{ ARCHIFY_BRAND_ALLOW_PRIVATE: '1' },
);
assert.notEqual(capture.status, 0, capture.stdout);
assert.match(capture.stderr, /do(?:es)? not match image\/png/i);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test('capture rejects a truncated PNG that contains only its signature', async () => {
const server = http.createServer((_request, response) => {
response.writeHead(200, { 'content-type': 'image/png' });
response.end(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const address = server.address();
const capture = await runCliAsync(
['brands', 'capture', `http://127.0.0.1:${address.port}/mark.png`, '--json'],
{ ARCHIFY_BRAND_ALLOW_PRIVATE: '1' },
);
assert.notEqual(capture.status, 0, capture.stdout);
assert.match(capture.stderr, /do(?:es)? not match image\/png/i);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test('unknown preset names fail with a repairable public CLI diagnostic', () => {
const input = writeFixture('architecture', 'unknown-preset', 'open-aii');
const result = spawnSync(process.execPath, [cli, 'validate', 'architecture', input, '--json'], {
cwd: skillRoot,
encoding: 'utf8',
});
assert.equal(result.status, 1, result.stderr || result.stdout);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.ok, false);
assert.ok(receipt.diagnostics.some((entry) => entry.code === 'brand/unknown'));
assert.ok(receipt.diagnostics.some((entry) => entry.supportedFixes.some((fix) => fix.includes('archify brands'))));
});
test('viewer exposes brand identity to Passport and Finder while keeping source beacons clear', () => {
const template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
assert.match(template, /id="focus-brand" data-passport="brand" hidden/);
assert.match(template, /node\.getAttribute\('data-node-brand'\)/);
assert.match(template, /brandOffset = node\.hasAttribute\('data-node-brand'\) \? 24 : 0/);
assert.match(template, /sourceSearch \+ ' ' \+ text\)\.toLowerCase\(\) \+ ' ' \+ brand\.toLowerCase\(\)/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,157 @@
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-chapter-delta-preview-'));
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',
};
const PROOF_CASES = [
'agent-tool-call.workflow.json',
'production-deployment.architecture.json',
'cache-miss-request.sequence.json',
'release-delivery.workflow.json',
'incident-response.workflow.json',
'product-analytics.dataflow.json',
'async-job-roundtrip.sequence.json',
'event-stream.dataflow.json',
'agent-run.lifecycle.json',
'deployment-release.lifecycle.json',
'web-app.architecture.json',
];
function render(mode) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', CASES[mode]),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
function delta(previous, destination) {
const previousIds = new Set(previous);
const destinationIds = new Set(destination);
return {
stay: previous.filter((id) => destinationIds.has(id)),
enter: destination.filter((id) => !previousIds.has(id)),
leave: previous.filter((id) => !destinationIds.has(id)),
};
}
test('all five renderers inherit one viewer-only static Chapter Delta Preview', () => {
for (const mode of Object.keys(CASES)) {
const html = render(mode);
assert.match(html, /function chapterDelta\(previous, destination\)/, mode);
assert.match(html, /className = 'guided-view-chapter-delta'/, mode);
assert.match(html, /svg\.setAttribute\('data-chapter-preview', views\[index\]\.id\)/, mode);
assert.match(html, /data-chapter-preview-role/, mode);
assert.match(html, /transition: none !important/, mode);
assert.doesNotMatch(canonicalSvg(html), /data-chapter-preview|data-chapter-preview-role/, mode);
}
});
test('exact stable-ID set math powers truthful counts and the existing handoff', () => {
const html = render('workflow');
assert.match(html, /stay: previousFocus\.filter\(function \(id\) \{ return destinationIds\[id\]; \}\)/);
assert.match(html, /enter: destinationFocus\.filter\(function \(id\) \{ return !previousIds\[id\]; \}\)/);
assert.match(html, /leave: previousFocus\.filter\(function \(id\) \{ return !destinationIds\[id\]; \}\)/);
assert.match(html, /var delta = chapterDelta\(previous, destination\);[\s\S]*?chapterAnchor\(previous, destination, outgoingBeatIndex, delta\)/);
assert.match(html, /var compact = '=' \+ delta\.stay\.length \+ ' \+' \+ delta\.enter\.length \+ ' \\u2212' \+ delta\.leave\.length/);
assert.match(html, /viewerText\('viewer\.guided\.chapter\.delta\.aria'/);
assert.doesNotMatch(html, /inferChapterDelta|matchChapterLabel|nearestKind/);
let adjacent = 0;
let shared = 0;
for (const file of PROOF_CASES) {
const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', file), 'utf8'));
const views = doc.meta.views;
for (let index = 1; index < views.length; index += 1) {
adjacent += 1;
if (delta(views[index - 1].focus, views[index].focus).stay.length) shared += 1;
}
}
assert.equal(adjacent, 22);
assert.equal(shared, 19);
const workflow = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', CASES.workflow), 'utf8'));
const [first, second, third] = workflow.meta.views;
assert.deepEqual(delta(first.focus, second.focus), {
stay: ['router', 'approval'],
enter: ['blocked', 'retry'],
leave: ['user', 'chat', 'planner', 'tool', 'external', 'final'],
});
assert.deepEqual(delta(second.focus, third.focus), {
stay: [],
enter: ['external', 'store', 'trace'],
leave: ['router', 'approval', 'blocked', 'retry'],
});
});
test('pointer and keyboard inspect while touch and native activation still commit directly', () => {
const html = render('architecture');
assert.match(html, /chapterList\.addEventListener\('pointerover'/);
assert.match(html, /!hoverCapable\(\) \|\| event\.pointerType === 'touch'/);
assert.match(html, /setChapterPreviewIntent\('pointer', chapterButtons\.indexOf\(button\)\)/);
assert.match(html, /chapterIndex\.addEventListener\('focusin',[\s\S]*?setChapterPreviewIntent\('focus'/);
assert.match(html, /event\.key === 'Escape' && activePreviewIndex >= 0[\s\S]*?event\.stopImmediatePropagation\(\)[\s\S]*?clearChapterPreview\(\{ clearIntents: true \}\)/);
assert.match(html, /chapterList\.addEventListener\('click',[\s\S]*?activateById/);
assert.match(html, /button\.type = 'button'/);
assert.doesNotMatch(html, /firstTap|secondTap|longpress|long-press/);
const previewRuntime = html.slice(
html.indexOf('function chapterPreviewBlocked()'),
html.indexOf('function sharePlaybackRequested()'),
);
assert.doesNotMatch(previewRuntime, /Archify\.view\.|updateUrl\(|Archify\.focus\.|renderStoryTrail\(/);
});
test('latest intent, stronger owners, playback, and lifecycle cleanup remain bounded', () => {
const html = render('lifecycle');
assert.match(html, /var previewGeneration = 0/);
assert.match(html, /right\.generation - left\.generation/);
assert.match(html, /\[pointerPreviewIntent, focusPreviewIntent\]/);
assert.match(html, /document\.hidden \|\| playing \|\| currentHandoff/);
assert.match(html, /data-route-picking'[\s\S]*?data-route-active/);
assert.match(html, /data-lens-active'[\s\S]*?data-legend-preview-active/);
assert.match(html, /data-relationship-preview-active'[\s\S]*?data-intent-trace-active/);
assert.match(html, /if \(playing\) pausePlayback\(\)/);
assert.match(html, /Archify\.motionGovernor\.claim\('chapter-preview'/);
assert.match(html, /Archify\.motionGovernor\.release\(token\)/);
assert.match(html, /handoff\.resolve[\s\S]*?syncChapterPreview\(\)/);
assert.match(html, /visibilitychange'[\s\S]*?clearChapterPreview/);
assert.match(html, /beforeprint'[\s\S]*?clearChapterPreview[\s\S]*?settleHandoff/);
});
test('mobile, Still, embed, print, and canonical exports keep the preview viewer-only', () => {
const html = render('sequence');
assert.match(html, /\.guided-view-chapter \{ min-height: 2\.75rem; \}/);
assert.match(html, /flex: 0 0 min\(14rem, 78vw\)/);
assert.match(html, /\.guided-view-chapter-delta\[hidden\] \{ display: none; \}/);
assert.match(html, /document\.documentElement\.getAttribute\('data-embed'\) === 'true'/);
assert.match(html, /prefers-reduced-motion: reduce[\s\S]*?svg\[data-chapter-preview\]/);
assert.match(html, /svg\[data-chapter-preview\] \[data-node-id\],[\s\S]*?opacity: 1 !important; filter: none !important/);
assert.match(html, /clone\.removeAttribute\('data-chapter-preview'\)/);
assert.match(html, /clone\.querySelectorAll\('\[data-chapter-preview-role\]'\)/);
assert.match(html, /!clone\.hasAttribute\('data-chapter-preview'\)/);
assert.match(html, /canonicalStateClean/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,97 @@
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-chapter-handoff-'));
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) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', CASES[mode]),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all five renderers inherit one viewer-only Shared Anchor Chapter Handoff', () => {
for (const mode of Object.keys(CASES)) {
const html = render(mode);
assert.match(html, /id="guided-view-handoff" hidden aria-hidden="true"/, mode);
assert.match(html, /function beginHandoff\(previousIndex, nextIndex, previous, destination, outgoingBeatIndex, reason\)/, mode);
assert.match(html, /data-chapter-handoff-overlay/, mode);
assert.match(html, /ring\.setAttribute\('class', 'chapter-handoff-anchor'\)/, mode);
assert.doesNotMatch(canonicalSvg(html), /data-chapter-handoff|data-chapter-role|chapter-handoff-anchor/, mode);
}
});
test('anchor selection uses exact stable-id intersection with deterministic outgoing priority', () => {
const html = render('workflow');
assert.match(html, /function chapterDelta\(previous, destination\)/);
assert.match(html, /function chapterAnchor\(previous, destination, outgoingBeatIndex, delta\)/);
assert.match(html, /delta\.stay\.forEach\(function \(id\) \{ stayIds\[id\] = true; \}\)/);
assert.match(html, /var activeBeat = outgoingBeatIndex >= 0 \? previous\.focus\[outgoingBeatIndex\] : ''/);
assert.match(html, /activeBeat && stayIds\[activeBeat\]/);
assert.match(html, /for \(var index = previous\.focus\.length - 1; index >= 0; index -= 1\)/);
assert.match(html, /if \(stayIds\[previous\.focus\[index\]\]\) return previous\.focus\[index\]/);
assert.doesNotMatch(html, /inferChapterAnchor|matchChapterLabel|nearestKind/);
});
test('handoff holds one truthful anchor then settles through one finite camera transaction', () => {
const html = render('architecture');
assert.match(html, /handoff\.mode = 'settling'/);
assert.match(html, /setTimeout\(startCamera, 110\)/);
assert.match(html, /duration: 420/);
assert.match(html, /requestAnimationFrame\(step\)/);
assert.match(html, /var eased = 1 - Math\.pow\(1 - fraction, 3\)/);
assert.match(html, /Archify\.motionGovernor\.claim\('handoff'/);
assert.match(html, /Archify\.motionGovernor\.release\(handoff\.ownerToken\)/);
assert.match(html, /handoffReceipt\.textContent = viewerText\('viewer\.guided\.handoff'/);
assert.doesNotMatch(html, /chapter-handoff[^\n]+infinite/);
});
test('latest intent, manual takeover, Still, reduced motion, and hidden pages cleanly settle', () => {
const html = render('lifecycle');
assert.match(html, /cancelHandoff\('replaced'\)/);
assert.match(html, /cameraTransaction\.cancel\(reason \|\| 'cancelled', commitTarget === true\)/);
assert.match(html, /transaction\.settled/);
assert.match(html, /currentHandoff !== handoff/);
assert.match(html, /Archify\.guidedViews\.cancelHandoff\(reason \|\| 'manual'\)/);
assert.match(html, /settleHandoff\(systemPaused \? 'reduced-motion' : \(hasSuspension\(\) \? 'hidden' : 'still'\)\)/);
assert.match(html, /settleHandoff\('hidden'\)/);
assert.match(html, /settleHandoff\('reduced-motion'\)/);
assert.match(html, /window\.addEventListener\('beforeprint',[\s\S]*?clearChapterPreview[\s\S]*?settleHandoff\('print'\)/);
});
test('mobile, embed, print, and canonical exports keep strict static boundaries', () => {
const html = render('sequence');
assert.match(html, /document\.documentElement\.getAttribute\('data-embed'\) === 'true'/);
assert.match(html, /cameraReceipt\(\{ scrollLeft: target \}/);
assert.match(html, /behavior: instant \? 'auto' : 'smooth'/);
assert.match(html, /\.chapter-handoff-overlay \{ display: none !important; \}/);
assert.match(html, /clone\.removeAttribute\('data-chapter-handoff'\)/);
assert.match(html, /clone\.removeAttribute\('data-chapter-anchor'\)/);
assert.match(html, /clone\.querySelectorAll\('\[data-chapter-handoff-overlay\]'\)/);
assert.match(html, /clone\.querySelectorAll\('\[data-chapter-role\]'\)/);
assert.match(html, /!clone\.hasAttribute\('data-chapter-handoff'\)/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,86 @@
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-chapter-rail-'));
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) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', CASES[mode]),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all guided renderers expose one runtime-built named chapter rail', () => {
for (const mode of Object.keys(CASES)) {
const html = render(mode);
assert.match(html, /<nav class="guided-view-index" id="guided-view-index" aria-label="Story chapters">/, mode);
assert.match(html, /<ol class="guided-view-chapters" id="guided-view-chapters"><\/ol>/, mode);
assert.match(html, /function buildChapterIndex\(\)/, mode);
assert.match(html, /views\.forEach\(function \(view, index\)/, mode);
assert.match(html, /position\.textContent = \(index \+ 1 < 10 \? '0' : ''\) \+ \(index \+ 1\)/, mode);
assert.match(html, /title\.textContent = view\.label/, mode);
assert.match(html, /stops\.textContent = viewerCount\('viewer\.guided\.chapter\.stop', view\.focus\.length\)/, mode);
assert.doesNotMatch(canonicalSvg(html), /guided-view-chapter|data-chapter-position/, mode);
}
});
test('chapter rail delegates selection and mirrors the existing activeIndex owner', () => {
const html = render('architecture');
assert.match(html, /activateById\(button\.getAttribute\('data-guided-view-id'\)\)/);
assert.match(html, /var current = index === activeIndex/);
assert.match(html, /activeIndex < 0 \? 'available' : \(current \? 'current' : \(index < activeIndex \? 'before' : 'after'\)\)/);
assert.match(html, /button\.setAttribute\('aria-current', 'step'\)/);
assert.match(html, /button\.removeAttribute\('aria-current'\)/);
assert.match(html, /syncChapterIndex\(\);[\s\S]*renderShareCue\(\)/);
assert.doesNotMatch(html, /selectedChapter|visitedChapters|completedChapters/);
});
test('chapter rail is keyboard-first and pauses playback on reader takeover', () => {
const html = render('workflow');
assert.match(html, /chapterIndex\.addEventListener\('focusin',[\s\S]*if \(playing\) pausePlayback\(\)/);
assert.match(html, /event\.key === 'ArrowRight'/);
assert.match(html, /event\.key === 'ArrowLeft'/);
assert.match(html, /event\.key === 'Home'/);
assert.match(html, /event\.key === 'End'/);
assert.match(html, /focusChapterButton\(target\)/);
assert.match(html, /button\.type = 'button'/);
assert.doesNotMatch(html, /role="tab"|role="tabpanel"/);
});
test('chapter rail has positional, touch, mobile, motion, embed, and print boundaries', () => {
const html = render('lifecycle');
assert.match(html, /\.guided-view-chapter\[data-chapter-position="current"\][\s\S]*border: 2px solid/);
assert.match(html, /data-chapter-position="before"[\s\S]*border-style: solid/);
assert.match(html, /data-chapter-position="after"[\s\S]*border-style: dashed/);
assert.match(html, /\.guided-view-chapter \{ min-height: 2\.75rem; \}/);
assert.match(html, /scroll-snap-type: x proximity/);
assert.match(html, /flex: 0 0 min\(14rem, 78vw\)/);
assert.match(html, /behavior: 'auto'/);
assert.match(html, /prefers-reduced-motion: reduce[\s\S]*\.guided-view-chapter \{ transition: none !important; \}/);
assert.match(html, /html\[data-embed="true"\] \.guided-views \{ display: none !important; \}/);
assert.match(html, /\.toolbar, \.diagram-nav, \.focus-chip, \.guided-views, \.archify-toast, \.no-print \{ display: none !important; \}/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,244 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { stageCleanSkill } from '../../scripts/stage-clean-skill.mjs';
const stagerPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../scripts/stage-clean-skill.mjs');
function git(root, args) {
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
assert.equal(result.status, 0, result.stderr);
}
function write(root, relative, content, mode = null) {
const target = path.join(root, relative);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, content);
if (mode !== null) fs.chmodSync(target, mode);
return target;
}
function repositoryFixture() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-clean-stage-'));
write(root, 'archify/package.json', JSON.stringify({
name: 'archify-fixture',
scripts: { test: 'node --test' },
devDependencies: { ajv: '1.0.0' },
}));
write(root, 'archify/package-lock.json', '{}\n');
write(root, 'archify/skill-release.json', '{}\n');
write(root, 'archify/scripts/check-update.mjs', 'export {};\n');
write(root, 'archify/scripts/update-contract.mjs', 'export {};\n');
write(root, 'archify/renderers/shared/generated-validators.mjs', 'export {};\n');
write(root, 'archify/test/repository-only.test.mjs', 'throw new Error();\n');
git(root, ['init']);
return root;
}
test('clean staging preserves index modes and strips repository-only package metadata', () => {
const root = repositoryFixture();
const destination = path.join(root, 'staged-skill');
try {
write(root, 'archify/bin/executable.mjs', '#!/usr/bin/env node\n', 0o755);
write(root, 'archify/runtime/test/required.dat', 'runtime fixture\n');
git(root, ['add', 'archify']);
stageCleanSkill({ repoRoot: root, destination });
assert.equal(fs.statSync(path.join(destination, 'bin', 'executable.mjs')).mode & 0o777, 0o755);
assert.equal(fs.existsSync(path.join(destination, 'test')), false);
assert.equal(
fs.readFileSync(path.join(destination, 'runtime', 'test', 'required.dat'), 'utf8'),
'runtime fixture\n',
'only the repository-root test tree is excluded',
);
assert.equal(fs.existsSync(path.join(destination, 'package-lock.json')), false);
const packageJson = JSON.parse(fs.readFileSync(path.join(destination, 'package.json'), 'utf8'));
assert.equal(Object.hasOwn(packageJson, 'scripts'), false);
assert.equal(Object.hasOwn(packageJson, 'devDependencies'), false);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('clean staging rejects a symlink in a tracked file ancestor before copying bytes', (t) => {
const root = repositoryFixture();
const destination = path.join(root, 'staged-skill');
try {
const runtime = path.join(root, 'archify', 'runtime');
write(root, 'archify/runtime/payload.txt', 'tracked fixture\n');
git(root, ['add', 'archify']);
fs.rmSync(runtime, { recursive: true });
const external = path.join(root, 'outside-runtime');
write(root, 'outside-runtime/payload.txt', 'external secret\n');
try {
fs.symlinkSync(external, runtime, process.platform === 'win32' ? 'junction' : 'dir');
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`symlinks unavailable: ${error.code}`);
return;
}
throw error;
}
assert.throws(
() => stageCleanSkill({ repoRoot: root, destination }),
/refusing to package path through symlink: archify\/runtime/,
);
assert.equal(fs.existsSync(destination), false);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('clean staging rejects tracked symlinks before reading through them', (t) => {
const root = repositoryFixture();
const destination = path.join(root, 'staged-skill');
try {
const external = write(root, 'outside.txt', 'private fixture\n');
const linked = path.join(root, 'archify', 'linked.txt');
try {
fs.symlinkSync(external, linked);
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`symlinks unavailable: ${error.code}`);
return;
}
throw error;
}
git(root, ['add', 'archify']);
assert.throws(
() => stageCleanSkill({ repoRoot: root, destination }),
/refusing to package tracked symlink: archify\/linked\.txt/,
);
assert.equal(fs.existsSync(destination), false);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('clean staging snapshots unstaged tracked bytes before a source ancestor can be swapped', (t) => {
const root = repositoryFixture();
const destination = path.join(root, 'staged-skill');
const runtime = path.join(root, 'archify', 'runtime');
const external = path.join(root, 'outside-runtime');
const originalMkdirSync = fs.mkdirSync;
let swapped = false;
try {
const payload = write(root, 'archify/runtime/payload.txt', 'indexed fixture\n');
write(root, 'outside-runtime/payload.txt', 'external secret\n');
git(root, ['add', 'archify']);
fs.writeFileSync(payload, 'unstaged working-tree fixture\n');
const probe = path.join(root, 'symlink-probe');
try {
fs.symlinkSync(external, probe, process.platform === 'win32' ? 'junction' : 'dir');
fs.rmSync(probe, { force: true });
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`symlinks unavailable: ${error.code}`);
return;
}
throw error;
}
fs.mkdirSync = function swapSourceAfterSnapshot(target, ...args) {
const result = originalMkdirSync.call(fs, target, ...args);
if (!swapped && path.resolve(target) === path.resolve(destination)) {
fs.rmSync(runtime, { recursive: true });
fs.symlinkSync(external, runtime, process.platform === 'win32' ? 'junction' : 'dir');
swapped = true;
}
return result;
};
stageCleanSkill({ repoRoot: root, destination });
assert.equal(swapped, true, 'the deterministic ancestor-swap attack must run');
assert.equal(
fs.readFileSync(path.join(destination, 'runtime', 'payload.txt'), 'utf8'),
'unstaged working-tree fixture\n',
'staging keeps the tracked working-tree snapshot and never follows the replacement ancestor',
);
} finally {
fs.mkdirSync = originalMkdirSync;
fs.rmSync(root, { recursive: true, force: true });
}
});
test('clean staging rejects a source ancestor swapped during preflight traversal', (t) => {
const root = repositoryFixture();
const destination = path.join(root, 'staged-skill');
const runtime = path.join(root, 'archify', 'runtime');
const external = path.join(root, 'outside-runtime');
const originalLstatSync = fs.lstatSync;
let swapped = false;
try {
write(root, 'archify/runtime/payload.txt', 'tracked fixture\n');
write(root, 'outside-runtime/payload.txt', 'external secret\n');
git(root, ['add', 'archify']);
const canonicalRuntime = path.join(fs.realpathSync(root), 'archify', 'runtime');
const probe = path.join(root, 'symlink-probe');
try {
fs.symlinkSync(external, probe, process.platform === 'win32' ? 'junction' : 'dir');
fs.rmSync(probe, { force: true });
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`symlinks unavailable: ${error.code}`);
return;
}
throw error;
}
fs.lstatSync = function swapSourceBetweenAncestorAndLeaf(target, ...args) {
const metadata = originalLstatSync.call(fs, target, ...args);
if (!swapped && path.resolve(target) === canonicalRuntime) {
// Guard before mutation: recursive removal can re-enter the patched
// lstatSync implementation on Linux.
swapped = true;
fs.rmSync(runtime, { recursive: true });
fs.symlinkSync(external, runtime, process.platform === 'win32' ? 'junction' : 'dir');
}
return metadata;
};
assert.throws(
() => stageCleanSkill({ repoRoot: root, destination }),
/(?:tracked package path changed before it could be read: archify\/|tracked package input is missing or unreadable: archify\/runtime\/payload\.txt)/,
);
assert.equal(swapped, true, 'the deterministic mid-preflight ancestor swap must run');
assert.equal(fs.existsSync(destination), false);
} finally {
fs.lstatSync = originalLstatSync;
fs.rmSync(root, { recursive: true, force: true });
}
});
test('clean staging reports the Git spawn error when Git cannot start', () => {
const root = repositoryFixture();
const destination = path.join(root, 'staged-skill');
try {
git(root, ['add', 'archify']);
const result = spawnSync(process.execPath, [
stagerPath,
'--root', root,
'--dest', destination,
], {
encoding: 'utf8',
env: { ...process.env, PATH: '' },
});
assert.notEqual(result.status, 0);
assert.match(result.stderr, /unable to enumerate tracked Archify files: .*ENOENT/);
assert.doesNotMatch(result.stderr, /tracked Archify paths must be valid UTF-8/);
assert.equal(fs.existsSync(destination), false);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
+785
View File
@@ -0,0 +1,785 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { extractSvgs, parseXml } from './helpers/xml.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-cli-'));
const cli = path.join(skillRoot, 'bin/archify.mjs');
function run(args, options = {}) {
return spawnSync(process.execPath, [cli, ...args], {
cwd: options.cwd || skillRoot,
encoding: 'utf8',
env: options.env || process.env,
});
}
function sha256(file) {
return createHash('sha256').update(fs.readFileSync(file)).digest('hex');
}
function makeFakeOpeners(name, { exitCode = 0 } = {}) {
const bin = path.join(tmp, name);
const log = path.join(bin, 'open-log.json');
fs.mkdirSync(bin, { recursive: true });
const source = `#!/usr/bin/env node
const fs = require('node:fs');
const target = process.argv[process.argv.length - 1];
fs.writeFileSync(process.env.ARCHIFY_TEST_OPEN_LOG, JSON.stringify({
argv: process.argv.slice(2),
target,
existed: fs.existsSync(target),
}));
process.exit(${exitCode});
`;
for (const command of ['open', 'xdg-open']) {
const executable = path.join(bin, command);
fs.writeFileSync(executable, source);
fs.chmodSync(executable, 0o755);
}
return {
log,
env: {
...process.env,
PATH: `${bin}${path.delimiter}${process.env.PATH || ''}`,
ARCHIFY_TEST_OPEN_LOG: log,
},
};
}
function copyInstalledSkill(target) {
fs.cpSync(skillRoot, target, {
recursive: true,
filter(source) {
const rel = path.relative(skillRoot, source);
return rel !== 'node_modules' && !rel.startsWith(`node_modules${path.sep}`)
&& rel !== 'test' && !rel.startsWith(`test${path.sep}`)
// Another test creates this short-lived directory under skillRoot so
// Ajv resolves from the checkout. Never copy a concurrently removed
// test fixture into an installed-skill simulation.
&& !rel.startsWith('.validator-check-');
},
});
}
test('cli: help lists commands and diagram types', () => {
const result = run(['--help']);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /archify render <type>/);
assert.match(result.stdout, /archify compare architecture <base\.json> <head\.json>/);
assert.match(result.stdout, /archify deliver <type>/);
assert.match(result.stdout, /archify preview <type>/);
assert.match(result.stdout, /archify visual-check <output\.html>/);
assert.match(result.stdout, /--open/);
assert.match(result.stdout, /--repo-root path \(architecture only\)/);
assert.match(result.stdout, /archify guide \[scenario or question\]/);
assert.match(result.stdout, /archify doctor/);
assert.match(result.stdout, /archify demo \[output-directory\]/);
assert.match(result.stdout, /architecture, workflow, sequence, dataflow, lifecycle/);
});
test('cli: doctor reports a complete installation is ready', () => {
const result = run(['doctor']);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /\[ok\] Node\.js v\d+/);
assert.match(result.stdout, /\[ok\] Core template/);
assert.match(result.stdout, /\[ok\] Example renderer/);
assert.match(result.stdout, /\[ok\] Live preview runtime/);
assert.match(result.stdout, /\[ok\] Scenario recipe guide/);
assert.match(result.stdout, /\[ok\] Progressive authoring references/);
assert.match(result.stdout, /\[ok\] Architecture compare runtime and proof fixtures/);
assert.match(result.stdout, /\[ok\] Standalone schema validators/);
assert.match(result.stdout, /\[ok\] architecture renderer, schema, and example/);
assert.match(result.stdout, /\[ok\] lifecycle renderer, schema, and example/);
assert.match(result.stdout, /Archify is ready\./);
});
test('cli: doctor identifies an incomplete installation', () => {
const incompleteRoot = path.join(tmp, 'incomplete-skill');
const incompleteBin = path.join(incompleteRoot, 'bin');
fs.mkdirSync(incompleteBin, { recursive: true });
fs.copyFileSync(cli, path.join(incompleteBin, 'archify.mjs'));
const result = spawnSync(process.execPath, [path.join(incompleteBin, 'archify.mjs'), 'doctor'], {
cwd: incompleteRoot,
encoding: 'utf8',
});
assert.equal(result.status, 1);
assert.match(result.stdout, /\[missing\] Core template/);
assert.match(result.stdout, /\[missing\] Scenario recipe guide/);
assert.match(result.stdout, /\[missing\] workflow renderer, schema, and example/);
assert.match(result.stderr, /Archify is not ready: \d+ required files? missing\./);
});
test('cli: doctor rejects a corrupt standalone validator', () => {
const corruptRoot = path.join(tmp, 'corrupt-skill');
copyInstalledSkill(corruptRoot);
fs.writeFileSync(path.join(corruptRoot, 'renderers/shared/generated-validators.mjs'), 'export const workflow = ;\n');
const result = spawnSync(process.execPath, [path.join(corruptRoot, 'bin/archify.mjs'), 'doctor'], {
cwd: corruptRoot,
encoding: 'utf8',
});
assert.equal(result.status, 1);
assert.match(result.stdout, /\[invalid\] Standalone schema validators/);
assert.match(result.stderr, /Archify is not ready: 1 runtime check failed\./);
});
test('cli: examples renders from an installed skill', () => {
const installedRoot = path.join(tmp, 'installed-skill');
copyInstalledSkill(installedRoot);
const result = spawnSync(process.execPath, [path.join(installedRoot, 'bin/archify.mjs'), 'examples'], {
cwd: installedRoot,
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
for (const output of [
'workflow-agent-tool-call-rendered.html',
'sequence-cache-miss-request.html',
'dataflow-product-analytics.html',
'lifecycle-agent-run.html',
'web-app-rendered.html',
]) {
assert.equal(fs.existsSync(path.join(installedRoot, 'examples', output)), true, output);
}
});
test('cli: guide lists all scenario recipes by diagram type', () => {
const result = run(['guide']);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /Archify scenario recipes \(11\)/);
for (const type of ['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']) {
assert.match(result.stdout, new RegExp(`\\[${type}\\]`));
}
});
test('cli: guide recommends a scenario as structured json', () => {
const result = run(['guide', 'Show an API request with Redis cache miss', '--json']);
assert.equal(result.status, 0, result.stderr);
const parsed = JSON.parse(result.stdout);
assert.equal(parsed.ok, true);
assert.equal(parsed.lang, 'en');
assert.equal(parsed.confidence, 'high');
assert.equal(parsed.recommendation.id, 'api-request');
assert.equal(parsed.recommendation.type, 'sequence');
});
test('cli: guide detects Chinese and explains the recommendation boundary', () => {
const result = run(['guide', '展示 Kafka topic 消费者组和死信队列']);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /推荐: 事件流拓扑 \[dataflow\]/);
assert.match(result.stdout, /不要这样用:/);
assert.match(result.stdout, /必须包含:/);
assert.match(result.stdout, /可直接复制的提示词:/);
});
test('cli: guide works from an installed skill without node_modules', () => {
const installedRoot = path.join(tmp, 'installed-guide-skill');
copyInstalledSkill(installedRoot);
const installedCli = path.join(installedRoot, 'bin/archify.mjs');
const result = spawnSync(process.execPath, [installedCli, 'guide', 'incident-runbook', '--json'], {
cwd: installedRoot,
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
assert.equal(JSON.parse(result.stdout).recommendation.id, 'incident-runbook');
});
test('cli: demo creates a ready-to-open diagram in a chosen directory', () => {
const outputDirectory = path.join(tmp, 'my-demo');
const output = path.join(outputDirectory, 'archify-demo.html');
const result = run(['demo', outputDirectory]);
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.existsSync(output), true);
assert.match(fs.readFileSync(output, 'utf8'), /Sample Web App Diagram/);
assert.match(result.stdout, new RegExp(`Demo ready: ${output.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
assert.match(result.stdout, /Next: open the HTML in your browser/);
assert.match(result.stdout, /archify render architecture/);
});
test('cli: demo defaults to the current directory', () => {
const workingDirectory = path.join(tmp, 'default-demo');
fs.mkdirSync(workingDirectory);
const result = run(['demo'], { cwd: workingDirectory });
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.existsSync(path.join(workingDirectory, 'archify-demo.html')), true);
});
test('cli: render writes a diagram html file', () => {
const out = path.join(tmp, 'workflow.html');
const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json');
const result = run(['render', 'workflow', input, out]);
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.existsSync(out), true);
assert.match(fs.readFileSync(out, 'utf8'), /Agent Tool Call Workflow/);
});
test('cli: visual-check returns a skipped receipt with exit 2 when Chrome is unavailable', () => {
const out = path.join(tmp, 'visual-check-skipped.html');
fs.writeFileSync(out, '<!doctype html><html><body>delivered</body></html>');
const missingChrome = path.join(tmp, 'missing-chrome');
const result = run(['visual-check', out, '--json'], {
env: { ...process.env, ARCHIFY_CHROME: missingChrome },
});
assert.equal(result.status, 2, result.stderr);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.status, 'skipped');
assert.equal(receipt.visualReview, 'pending');
assert.equal(receipt.chrome.status, 'unavailable');
assert.equal(fs.existsSync(out.replace(/\.html$/, '.visual-check.json')), true);
});
test('cli: deliver atomically writes a checked artifact and structured receipt', () => {
const out = path.join(tmp, 'delivered-workflow.html');
const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json');
const result = run(['deliver', 'workflow', input, out, '--quality', 'showcase', '--json']);
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.existsSync(out), true);
assert.match(fs.readFileSync(out, 'utf8'), /Agent Tool Call Workflow/);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.schemaVersion, 1);
assert.equal(receipt.ok, true);
assert.equal(receipt.command, 'deliver');
assert.equal(receipt.type, 'workflow');
assert.equal(receipt.input, input);
assert.equal(receipt.output, out);
assert.deepEqual(receipt.specification, {
sha256: sha256(input),
bytes: fs.statSync(input).size,
});
assert.match(receipt.artifact.sha256, /^[a-f0-9]{64}$/);
assert.equal(receipt.artifact.sha256, sha256(out));
assert.equal(receipt.artifact.bytes, fs.statSync(out).size);
assert.deepEqual(receipt.validation, {
checksPassed: 9,
checkCount: 9,
compositionProfile: 'showcase',
compositionStatus: 'pass',
errors: 0,
warnings: 0,
});
assert.equal('open' in receipt, false);
});
test('cli: deliver --open launches only the committed absolute artifact as one argument', {
skip: process.platform === 'win32',
}, () => {
const fake = makeFakeOpeners('successful-open');
const out = path.join(tmp, `-复杂 path 'quoted'`, 'verified diagram.html');
const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json');
const result = run(['deliver', 'workflow', input, out, '--open', '--json'], { env: fake.env });
assert.equal(result.status, 0, result.stderr);
const receipt = JSON.parse(result.stdout);
assert.deepEqual(receipt.open, {
requested: true,
status: 'opened',
target: out,
method: process.platform === 'darwin' ? 'open' : 'xdg-open',
});
const invocation = JSON.parse(fs.readFileSync(fake.log, 'utf8'));
assert.equal(invocation.existed, true, 'the opener must run after the atomic commit');
assert.deepEqual(invocation.argv, [out]);
assert.equal(invocation.target, out);
assert.equal(fs.existsSync(out), true);
});
test('cli: opener failure does not invalidate a verified delivery or pollute json stdout', {
skip: process.platform === 'win32',
}, () => {
const fake = makeFakeOpeners('failed-open', { exitCode: 17 });
const out = path.join(tmp, 'open-failure-preserves-delivery.html');
const input = path.join(skillRoot, 'examples/web-app.architecture.json');
const result = run(['deliver', 'architecture', input, out, '--open', '--json'], { env: fake.env });
assert.equal(result.status, 0, result.stderr);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.ok, true);
assert.equal(receipt.open.status, 'failed');
assert.equal(receipt.open.target, out);
assert.match(result.stderr, /Could not open the verified artifact/);
assert.match(result.stderr, new RegExp(out.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
assert.equal(fs.existsSync(out), true);
assert.equal(receipt.artifact.sha256, sha256(out));
});
test('cli: deliver failure never invokes the optional opener', {
skip: process.platform === 'win32',
}, () => {
const fake = makeFakeOpeners('never-open');
const input = path.join(tmp, 'invalid-open-delivery.json');
fs.writeFileSync(input, '{broken json');
const out = path.join(tmp, 'must-not-open.html');
const result = run(['deliver', 'architecture', input, out, '--open', '--json'], { env: fake.env });
assert.equal(result.status, 1);
assert.equal(JSON.parse(result.stdout).stage, 'input');
assert.equal(fs.existsSync(fake.log), false);
assert.equal(fs.existsSync(out), false);
});
test('cli: a missing optional opener module preserves verified delivery with a fallback receipt', () => {
const installedRoot = path.join(tmp, 'missing-open-module-skill');
copyInstalledSkill(installedRoot);
const installedCli = path.join(installedRoot, 'bin/archify.mjs');
fs.rmSync(path.join(installedRoot, 'bin/open-artifact.mjs'));
const input = path.join(installedRoot, 'examples/agent-tool-call.workflow.json');
const out = path.join(tmp, 'missing-open-module-delivery.html');
const result = spawnSync(process.execPath, [installedCli, 'deliver', 'workflow', input, out, '--open', '--json'], {
cwd: installedRoot,
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.ok, true);
assert.deepEqual(receipt.open, {
requested: true,
status: 'unsupported',
target: out,
method: null,
});
assert.match(result.stderr, /Open it manually/);
assert.equal(receipt.artifact.sha256, sha256(out));
});
test('cli: deliver preserves the renderer default output contract', () => {
const workingDirectory = path.join(tmp, 'delivery-default-output');
fs.mkdirSync(workingDirectory, { recursive: true });
const input = path.join(workingDirectory, 'source.architecture.json');
const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples/web-app.architecture.json'), 'utf8'));
source.meta.output = 'verified-default.html';
fs.writeFileSync(input, JSON.stringify(source));
const result = run(['deliver', 'architecture', input, '--json'], { cwd: workingDirectory });
assert.equal(result.status, 0, result.stderr);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.output, path.join(fs.realpathSync(workingDirectory), 'verified-default.html'));
assert.equal(fs.existsSync(receipt.output), true);
});
test('cli: deliver works from an installed skill without node_modules', () => {
const installedRoot = path.join(tmp, 'installed-deliver-skill');
copyInstalledSkill(installedRoot);
const installedCli = path.join(installedRoot, 'bin/archify.mjs');
const cases = [
['architecture-boundaries', 'architecture', 'production-deployment.architecture.json'],
['architecture-issue-110', 'architecture', 'brand-aware-delivery.architecture.json'],
['workflow', 'workflow', 'agent-tool-call.workflow.json'],
['sequence', 'sequence', 'cache-miss-request.sequence.json'],
['dataflow', 'dataflow', 'product-analytics.dataflow.json'],
['lifecycle', 'lifecycle', 'agent-run.lifecycle.json'],
];
for (const [label, type, example] of cases) {
const input = path.join(installedRoot, 'examples', example);
const out = path.join(tmp, `installed-${label}-delivery.html`);
const result = spawnSync(process.execPath, [installedCli, 'deliver', type, input, out, '--json'], {
cwd: installedRoot,
encoding: 'utf8',
});
assert.equal(result.status, 0, `${label}: ${result.stderr}`);
assert.equal(JSON.parse(result.stdout).validation.checkCount, 9, label);
assert.equal(fs.existsSync(out), true, label);
const extracted = extractSvgs(fs.readFileSync(out, 'utf8'));
assert.equal(extracted.direct.length, 1, `${label}: expected one delivered SVG`);
assert.doesNotThrow(
() => parseXml(extracted.direct[0]),
`${label}: delivered SVG must be well-formed XML`,
);
}
});
test('cli: deliver XML guard parses markup instead of scanning attribute-like text', () => {
assert.doesNotThrow(() => parseXml(
'<svg xmlns="http://www.w3.org/2000/svg" aria-label="mentions data-node-label safely"/>',
));
assert.throws(
() => parseXml('<svg xmlns="http://www.w3.org/2000/svg" data-node-label></svg>'),
/attribute without value/i,
);
assert.throws(
() => parseXml('<svg xmlns="http://www.w3.org/2000/svg"><g></svg>'),
/unexpected close tag/i,
);
});
test('cli: preview runs from an installed skill without node_modules and exits cleanly', { timeout: 30000 }, async () => {
const installedRoot = path.join(tmp, 'installed-preview-skill');
copyInstalledSkill(installedRoot);
const installedCli = path.join(installedRoot, 'bin/archify.mjs');
const input = path.join(installedRoot, 'examples/web-app.architecture.json');
const output = path.join(tmp, 'installed-preview.html');
const child = spawn(process.execPath, [installedCli, 'preview', 'architecture', input, output, '--quality', 'showcase', '--no-open'], {
cwd: installedRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
let previewUrl;
const started = Date.now();
while (!previewUrl && Date.now() - started < 8000) {
previewUrl = stdout.match(/preview (http:\/\/127\.0\.0\.1:\d+\/)/)?.[1];
if (!previewUrl) await new Promise((resolve) => setTimeout(resolve, 40));
}
assert.ok(previewUrl, `preview URL missing; stdout=${stdout}; stderr=${stderr}`);
let state;
while (Date.now() - started < 15000) {
state = await fetch(new URL('/state', previewUrl)).then((response) => response.json());
if (state.status === 'verified') break;
await new Promise((resolve) => setTimeout(resolve, 50));
}
assert.equal(state?.status, 'verified', `preview did not verify; stdout=${stdout}; stderr=${stderr}`);
assert.equal(state.revision, 1);
assert.equal(fs.existsSync(output), true);
child.kill('SIGTERM');
const exit = await new Promise((resolve) => child.once('close', (code, signal) => resolve({ code, signal })));
assert.deepEqual(exit, { code: 0, signal: null });
assert.match(stdout, /stopping preview/);
await assert.rejects(fetch(previewUrl));
assert.deepEqual(fs.readdirSync(path.dirname(output)).filter((name) => name.startsWith('.archify-preview-')), []);
});
test('cli: deliver preserves the previous artifact when the final check fails', () => {
const installedRoot = path.join(tmp, 'broken-deliver-skill');
copyInstalledSkill(installedRoot);
const installedCli = path.join(installedRoot, 'bin/archify.mjs');
const templatePath = path.join(installedRoot, 'assets/template.html');
const template = fs.readFileSync(templatePath, 'utf8');
fs.writeFileSync(templatePath, template.replace('</body>', '<svg aria-label="accidental second svg"></svg>\n</body>'));
const input = path.join(installedRoot, 'examples/web-app.architecture.json');
const out = path.join(tmp, 'preserved-delivery.html');
const trustedPriorArtifact = '<!doctype html><title>trusted prior artifact</title>\n';
fs.writeFileSync(out, trustedPriorArtifact);
const result = spawnSync(process.execPath, [installedCli, 'deliver', 'architecture', input, out, '--json'], {
cwd: installedRoot,
encoding: 'utf8',
});
assert.equal(result.status, 1);
const failure = JSON.parse(result.stdout);
assert.equal(failure.ok, false);
assert.equal(failure.stage, 'check');
assert.equal(failure.diagnostics[0].code, 'artifact/single-svg');
assert.equal(failure.diagnostics[0].subject.check, 'single_svg');
assert.ok(failure.diagnostics[0].supportedFixes.some((fix) => fix.includes('exactly one diagram SVG')));
assert.equal(failure.checker.checks.find((entry) => entry.name === 'single_svg').ok, false);
assert.equal(fs.readFileSync(out, 'utf8'), trustedPriorArtifact);
assert.deepEqual(
fs.readdirSync(path.dirname(out)).filter((name) => name.includes('.archify-delivery-')),
[],
);
});
test('cli: deliver reports renderer failure as json and preserves the previous artifact', () => {
const input = path.join(tmp, 'invalid-delivery.workflow.json');
const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples/agent-tool-call.workflow.json'), 'utf8'));
source.nodes[0].unexpected = true;
fs.writeFileSync(input, JSON.stringify(source));
const out = path.join(tmp, 'renderer-failure-preserved.html');
const trustedPriorArtifact = '<!doctype html><title>last known good</title>\n';
fs.writeFileSync(out, trustedPriorArtifact);
const result = run(['deliver', 'workflow', input, out, '--json']);
assert.equal(result.status, 1);
const failure = JSON.parse(result.stdout);
assert.equal(failure.ok, false);
assert.equal(failure.stage, 'render');
assert.match(failure.error, /schema validation failed/i);
assert.equal(fs.readFileSync(out, 'utf8'), trustedPriorArtifact);
});
test('cli: deliver reports unreadable input as json without touching the target', () => {
const input = path.join(tmp, 'malformed-delivery.json');
fs.writeFileSync(input, '{not valid json');
const out = path.join(tmp, 'malformed-input-preserved.html');
const trustedPriorArtifact = '<!doctype html><title>still trusted</title>\n';
fs.writeFileSync(out, trustedPriorArtifact);
const result = run(['deliver', 'architecture', input, out, '--json']);
assert.equal(result.status, 1);
const failure = JSON.parse(result.stdout);
assert.equal(failure.ok, false);
assert.equal(failure.stage, 'input');
assert.match(failure.error, /Could not read delivery input/);
assert.equal(fs.readFileSync(out, 'utf8'), trustedPriorArtifact);
});
test('cli: invalid source output metadata still fails inside the renderer', () => {
const workingDirectory = path.join(tmp, 'invalid-output-metadata');
fs.mkdirSync(workingDirectory, { recursive: true });
const input = path.join(workingDirectory, 'source.architecture.json');
const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples/web-app.architecture.json'), 'utf8'));
source.meta.output = 17;
fs.writeFileSync(input, JSON.stringify(source));
const out = path.join(workingDirectory, 'architecture.html');
const trustedPriorArtifact = '<!doctype html><title>metadata did not replace me</title>\n';
fs.writeFileSync(out, trustedPriorArtifact);
const result = run(['deliver', 'architecture', input, '--json'], { cwd: workingDirectory });
assert.equal(result.status, 1);
const failure = JSON.parse(result.stdout);
assert.equal(failure.stage, 'render');
assert.match(failure.error, /schema validation failed/i);
assert.equal(fs.readFileSync(out, 'utf8'), trustedPriorArtifact);
});
test('cli: deliver reports commit failure without a false success receipt', () => {
const input = path.join(skillRoot, 'examples/web-app.architecture.json');
const outputDirectory = path.join(tmp, 'commit-target-is-a-directory');
fs.mkdirSync(outputDirectory, { recursive: true });
const result = run(['deliver', 'architecture', input, outputDirectory, '--json']);
assert.equal(result.status, 1);
const failure = JSON.parse(result.stdout);
assert.equal(failure.ok, false);
assert.equal(failure.stage, 'commit');
assert.match(failure.error, /Could not commit verified delivery/);
assert.equal(fs.statSync(outputDirectory).isDirectory(), true);
assert.equal(fs.readdirSync(outputDirectory).length, 0);
});
test('cli: deliver reports preparation failure as json without touching the blocker', () => {
const input = path.join(skillRoot, 'examples/web-app.architecture.json');
const blockingFile = path.join(tmp, 'delivery-parent-is-a-file');
fs.writeFileSync(blockingFile, 'do not replace me');
const out = path.join(blockingFile, 'cannot-write.html');
const result = run(['deliver', 'architecture', input, out, '--json']);
assert.equal(result.status, 1);
const failure = JSON.parse(result.stdout);
assert.equal(failure.ok, false);
assert.equal(failure.stage, 'prepare');
assert.match(failure.error, /Could not create delivery directory/);
assert.equal(fs.readFileSync(blockingFile, 'utf8'), 'do not replace me');
});
test('cli: check validates rendered html', () => {
const out = path.join(tmp, 'workflow-check.html');
const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json');
assert.equal(run(['render', 'workflow', input, out]).status, 0);
const result = run(['check', out]);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /"ok": true/);
});
test('cli: validate emits structured json without keeping html output', () => {
const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json');
const before = new Set(fs.readdirSync(tmp));
const result = run(['validate', 'workflow', input, '--json']);
assert.equal(result.status, 0, result.stderr);
const parsed = JSON.parse(result.stdout);
assert.equal(parsed.ok, true);
assert.equal(parsed.type, 'workflow');
assert.equal(parsed.checks.length, 9);
assert.equal(parsed.composition.profile, 'showcase');
assert.deepEqual(parsed.composition.summary, { errors: 0, warnings: 0 });
assert.equal(parsed.composition.metrics.containerBorderRuns, 0);
assert.equal(parsed.composition.metrics.ambiguousCorridors, 0);
assert.deepEqual(new Set(fs.readdirSync(tmp)), before);
});
test('cli: validate JSON exposes only the primary v1 column-capacity diagnostic', () => {
const input = path.join(tmp, 'pinned-column-capacity.workflow.json');
fs.writeFileSync(input, `${JSON.stringify({
schema_version: 1,
diagram_type: 'workflow',
meta: {
title: 'Pinned issue 126 diagnostic boundary',
viewBox: [720, 400],
legend: { mode: 'hidden' },
},
lanes: [{ id: 'main', label: 'Main' }],
nodes: [
{ id: 'a', lane: 'main', col: 1, type: 'backend', label: 'A' },
{ id: 'b', lane: 'main', col: 2, type: 'backend', label: 'B' },
],
edges: [{
id: 'ab',
from: 'a',
to: 'b',
fromSide: 'top',
toSide: 'top',
via: [[220, 60], [300, 60]],
}],
}, null, 2)}\n`);
const result = run(['validate', 'workflow', input, '--json'], {
env: { ...process.env, ARCHIFY_DIAGNOSTIC_FORMAT: 'json' },
});
assert.equal(result.status, 1, result.stderr || result.stdout);
assert.equal(result.stderr, '');
const failure = JSON.parse(result.stdout);
assert.equal(failure.ok, false);
assert.equal(failure.command, 'validate');
assert.equal(failure.stage, 'render');
assert.equal(failure.type, 'workflow');
assert.equal(failure.diagnostics.length, 1, JSON.stringify(failure.diagnostics, null, 2));
const [primary] = failure.diagnostics;
assert.equal(primary.code, 'workflow/column-capacity');
assert.equal(primary.subject.edge, 'ab');
assert.equal(primary.subject.fromCol, 1);
assert.equal(primary.subject.toCol, 2);
assert.ok(primary.supportedFixes.length > 0);
assert.ok(failure.diagnostics.every(({ code }) => (
code !== 'workflow/explicit-pin-conflict' && code !== 'workflow/viewbox-capacity'
)));
});
test('cli: --quality overrides the source profile for render, validate, and deliver', () => {
const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json');
const out = path.join(tmp, 'workflow-standard.html');
const rendered = run(['render', 'workflow', input, out, '--quality', 'standard']);
assert.equal(rendered.status, 0, rendered.stderr);
assert.match(fs.readFileSync(out, 'utf8'), /data-quality-profile="standard"/);
const validated = run(['validate', 'workflow', input, '--quality=standard', '--json']);
assert.equal(validated.status, 0, validated.stderr);
assert.equal(JSON.parse(validated.stdout).composition.profile, 'standard');
const deliveredOut = path.join(tmp, 'workflow-delivered-standard.html');
const delivered = run(['deliver', 'workflow', input, deliveredOut, '--quality=standard', '--json']);
assert.equal(delivered.status, 0, delivered.stderr);
assert.equal(JSON.parse(delivered.stdout).validation.compositionProfile, 'standard');
});
test('cli: rejects an unknown quality profile', () => {
const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json');
const result = run(['validate', 'workflow', input, '--quality', 'hero']);
assert.equal(result.status, 2);
assert.match(result.stderr, /Expected standard or showcase/);
});
test('cli: rejects a quality flag without a value', () => {
const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json');
for (const args of [
['validate', 'workflow', input, '--json', '--quality'],
['validate', 'workflow', input, '--quality', '--json'],
['validate', 'workflow', input, '--quality='],
]) {
const result = run(args);
assert.equal(result.status, 2);
assert.match(result.stderr, /--quality requires standard or showcase/);
}
});
test('cli: validate rejects unknown flags, layout-json assignment typos, and extra positionals', () => {
const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json');
const cases = [
{
args: ['validate', 'workflow', input, '--layout-json', '--bogus'],
pattern: /Unknown validate option "--bogus"/,
},
{
args: ['validate', 'workflow', input, '--layout-json=true'],
pattern: /Unknown validate option "--layout-json=true"/,
},
{
args: ['validate', 'workflow', input, '--layout-json=true', '--json'],
pattern: /Unknown validate option "--layout-json=true"/,
},
{
args: ['validate', 'workflow', input, 'unexpected-output.html', '--layout-json'],
pattern: /Usage:/,
},
];
for (const { args, pattern } of cases) {
const result = run(args);
assert.equal(result.status, 2, `${args.join(' ')}\n${result.stderr}\n${result.stdout}`);
assert.equal(result.stdout, '');
assert.match(result.stderr, pattern);
}
});
test('cli: inspect emits architecture layout json', () => {
const input = path.resolve(skillRoot, '../examples/archify-repo-grid.architecture.json');
const result = run(['inspect', 'architecture', input]);
assert.equal(result.status, 0, result.stderr);
const parsed = JSON.parse(result.stdout);
assert.equal(parsed.ok, true);
assert.equal(parsed.diagram_type, 'architecture');
assert.equal(parsed.layout.mode, 'grid');
assert.ok(parsed.components.length >= 5);
assert.ok(parsed.connections.length >= 1);
});
test('cli: inspect remains architecture-only while workflow uses validate --layout-json', () => {
const input = path.join(skillRoot, 'examples', 'agent-tool-call.workflow.json');
const result = run(['inspect', 'workflow', input]);
assert.equal(result.status, 2);
assert.match(result.stderr, /inspect is currently supported for architecture diagrams only/);
assert.equal(result.stdout, '');
});
test('cli: validate returns renderer errors for bad input', () => {
const input = path.join(tmp, 'bad.workflow.json');
const validateTmp = path.join(tmp, 'validate-failure-tmp');
const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples/agent-tool-call.workflow.json'), 'utf8'));
doc.edges[0].to = 'ghost';
fs.writeFileSync(input, JSON.stringify(doc));
fs.mkdirSync(validateTmp);
const result = run(['validate', 'workflow', input], {
env: { ...process.env, TMPDIR: validateTmp },
});
assert.notEqual(result.status, 0);
assert.match(result.stderr, /unknown target "ghost"/);
assert.deepEqual(fs.readdirSync(validateTmp), []);
});
test('cli: validate rejects an unknown type without leaking a temp directory', () => {
const validateTmp = path.join(tmp, 'validate-unknown-type-tmp');
fs.mkdirSync(validateTmp);
const result = run(['validate', 'unknown', 'ignored.json'], {
env: {
...process.env,
TMPDIR: validateTmp,
TMP: validateTmp,
TEMP: validateTmp,
},
});
assert.equal(result.status, 2);
assert.match(result.stderr, /Unknown diagram type "unknown"/);
assert.deepEqual(fs.readdirSync(validateTmp), []);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,104 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, '../..');
function read(relativePath) {
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
}
test('showcase intake requires reproducible proof, redaction, and explicit publication permission', () => {
const template = read('.github/ISSUE_TEMPLATE/showcase.yml');
for (const field of [
'id: diagram_type',
'id: archify_version',
'id: agent',
'id: model',
'id: prompt',
'id: source_json',
'id: artifact',
'id: validation_receipt',
'id: visual_review',
'id: sensitive_data',
'id: sharing_rights',
'id: public_permission',
]) {
assert.match(template, new RegExp(field), field);
}
assert.match(template, /access tokens/i);
assert.match(template, /personal or customer data/i);
assert.match(template, /repository, documentation, gallery, and project website/i);
assert.match(template, /required:\s*true/g);
const submissionUrl = 'https://github.com/tt-a1i/archify/issues/new?template=showcase.yml';
for (const readme of ['README.md', 'README_EN.md', 'README_ZH.md']) {
assert.match(read(readme), new RegExp(submissionUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), `${readme}: direct showcase link`);
}
});
test('bug intake captures a minimal deterministic reproduction before visual diagnosis', () => {
const template = read('.github/ISSUE_TEMPLATE/bug-report.yml');
for (const field of [
'id: archify_version',
'id: install_method',
'id: diagram_type',
'id: command',
'id: minimal_json',
'id: validation_receipt',
'id: expected',
'id: actual',
'id: environment',
'id: sensitive_data',
]) {
assert.match(template, new RegExp(field), field);
}
assert.ok(
template.indexOf('id: validation_receipt') < template.indexOf('id: screenshot'),
'deterministic evidence should be requested before optional visual evidence',
);
});
test('contributor and pull-request guides keep proof changes reproducible and stability-first', () => {
const contributing = read('CONTRIBUTING.md');
const pullRequest = read('.github/PULL_REQUEST_TEMPLATE.md');
for (const required of [
'.github/ISSUE_TEMPLATE/showcase.yml',
'.github/ISSUE_TEMPLATE/bug-report.yml',
'npm test',
'node scripts/build-gallery.mjs docs',
'Do not include secrets',
'Agent-first',
'diagnostics[]',
'Start from the latest `main`',
'tracked-only, symlink-safe',
'is **skipped**, not passed',
]) {
assert.match(contributing, new RegExp(required.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), required);
}
assert.match(
contributing,
/(?:^|\n)scripts\/build-zip\.sh \/tmp\/archify-contrib\.zip(?:\n|$)/,
'the archive builder must be documented as an executable shell script',
);
assert.doesNotMatch(
contributing,
/\bnode\s+scripts\/build-zip\.sh\b/,
'the shell archive builder must not be documented as a Node.js command',
);
for (const required of [
'Stability impact',
'Tests run',
'Generated artifacts',
'Visual evidence',
'No unrelated changes',
]) {
assert.match(pullRequest, new RegExp(required), required);
}
});
@@ -0,0 +1,74 @@
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 repoRoot = path.resolve(skillRoot, '..');
const cursorCommand = 'npx -y skills add tt-a1i/archify --skill archify --agent cursor --global --copy --yes';
test('Cursor onboarding stays explicit, bilingual, and backed by the same Skill', () => {
const english = fs.readFileSync(path.join(repoRoot, 'README.md'), 'utf8');
const englishMirror = fs.readFileSync(path.join(repoRoot, 'README_EN.md'), 'utf8');
const chinese = fs.readFileSync(path.join(repoRoot, 'README_ZH.md'), 'utf8');
const start = fs.readFileSync(path.join(repoRoot, 'docs', 'start.html'), 'utf8');
const landing = fs.readFileSync(path.join(repoRoot, 'docs', 'index.html'), 'utf8');
assert.equal(english, englishMirror, 'English README mirrors must stay synchronized');
assert.match(english, /Cursor, Claude Code, Codex CLI, and OpenCode/);
assert.match(chinese, /Cursor、Claude Code、Codex CLI 和 OpenCode/);
for (const surface of [english, chinese, landing]) assert.ok(surface.includes(cursorCommand));
for (const surface of [english, chinese, start, landing]) {
assert.doesNotMatch(surface, /skills use[^\n<]*--agent cursor/);
assert.doesNotMatch(surface, /~\/\.cursor\/skills\/archify/);
assert.doesNotMatch(surface, /all Cursor models|every Cursor model/i);
}
assert.match(start, /data-agent="cursor">Cursor<\/button>/);
assert.match(start, /data-agent="codex">Codex<\/button>/);
assert.match(start, /data-agent="claude-code">Claude Code<\/button>/);
assert.match(start, /data-agent="opencode">OpenCode<\/button>/);
assert.match(start, /KNOWN_AGENTS\.has\(requestedAgent\)/);
assert.match(start, /same Skill/);
assert.match(start, /同一份 Skill/);
assert.doesNotMatch(start, /vendor-specific (?:renderer|schema|skill)/i);
});
test('the zero-dependency archive works from the canonical Cursor-visible agent path', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-cursor-package-'));
const agentSkills = path.join(tmp, '.agents', 'skills');
try {
fs.mkdirSync(agentSkills, { recursive: true });
execFileSync('unzip', ['-q', path.join(repoRoot, 'archify.zip'), '-d', agentSkills]);
const installed = path.join(agentSkills, 'archify');
const cli = path.join(installed, 'bin', 'archify.mjs');
const doctor = execFileSync(process.execPath, [cli, 'doctor'], { encoding: 'utf8' });
assert.match(doctor, /Archify is ready\./);
const fixtures = {
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',
};
for (const [type, fixture] of Object.entries(fixtures)) {
const output = execFileSync(process.execPath, [
cli,
'validate',
type,
path.join(installed, 'examples', fixture),
'--json',
], { encoding: 'utf8' });
const receipt = JSON.parse(output);
assert.equal(receipt.ok, true, `${type}: installed package validation failed`);
assert.equal(receipt.type, type);
}
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
@@ -0,0 +1,161 @@
// Installation contract: the shipped skill performs full JSON Schema
// validation without node_modules. AJV is a build-time dependency only; its
// standalone validators are committed and included in the distribution.
//
// A malformed-but-JSON-legal document must EXIT
// NON-ZERO with a friendly message — never crash (TypeError / is not a
// function) and never write NaN/undefined into the HTML. A random VALID
// perturbation of an example must still render (exit 0, no NaN).
//
// node --test test/*.test.mjs
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-degraded-'));
const EXAMPLES = {
workflow: 'agent-tool-call.workflow.json',
sequence: 'cache-miss-request.sequence.json',
dataflow: 'product-analytics.dataflow.json',
lifecycle: 'agent-run.lifecycle.json',
architecture: 'web-app.architecture.json',
};
const installedRoot = path.join(tmp, 'installed-skill');
fs.cpSync(skillRoot, installedRoot, {
recursive: true,
filter(source) {
const rel = path.relative(skillRoot, source);
return rel !== 'node_modules' && !rel.startsWith(`node_modules${path.sep}`)
&& rel !== 'test' && !rel.startsWith(`test${path.sep}`)
// The validator freshness test creates and removes this fixture inside
// skillRoot while the test runner executes files concurrently. Exclude
// it from the installed-skill copy to avoid a copy/remove race.
&& !rel.startsWith('.validator-check-');
},
});
function render(mode, doc) {
const input = path.join(tmp, `in-${Math.random().toString(36).slice(2)}.json`);
const out = path.join(tmp, 'out.html');
fs.writeFileSync(input, JSON.stringify(doc));
if (fs.existsSync(out)) fs.rmSync(out);
let code = 0;
let stderr = '';
try {
execFileSync('node', [path.join(installedRoot, `renderers/${mode}/render-${mode}.mjs`), input, out],
{ stdio: ['ignore', 'ignore', 'pipe'] });
} catch (err) {
code = err.status ?? 1;
stderr = String(err.stderr || '');
}
const html = fs.existsSync(out) ? fs.readFileSync(out, 'utf8') : '';
return { code, stderr, html };
}
function assertFriendlyFailure(mode, doc, label) {
const { code, stderr, html } = render(mode, doc);
assert.notEqual(code, 0, `${label}: expected non-zero exit`);
assert.doesNotMatch(stderr, /TypeError|RangeError|is not a function|Cannot read/,
`${label}: crashed instead of reporting friendly error:\n${stderr}`);
assert.doesNotMatch(html, /NaN|undefined/, `${label}: wrote NaN/undefined into HTML`);
}
// ---- type-wrong-but-JSON-legal documents per mode ----
const ARRAY_FIELDS = {
workflow: ['lanes', 'phases', 'groups', 'mainPath', 'nodes', 'edges', 'cards'],
sequence: ['participants', 'messages', 'segments', 'activations', 'cards'],
dataflow: ['stages', 'nodes', 'flows', 'cards'],
lifecycle: ['lanes', 'states', 'transitions', 'cards'],
architecture: ['components', 'boundaries', 'connections', 'cards'],
};
for (const [mode, fields] of Object.entries(ARRAY_FIELDS)) {
for (const field of fields) {
test(`${mode}: ${field} as a string fails friendly`, () => {
const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', EXAMPLES[mode]), 'utf8'));
if (!(field in doc)) return; // optional field absent in this example
doc[field] = 'oops';
assertFriendlyFailure(mode, doc, `${mode}.${field}`);
});
}
test(`${mode}: scalar meta fails friendly`, () => {
const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', EXAMPLES[mode]), 'utf8'));
doc.meta = 42;
assertFriendlyFailure(mode, doc, `${mode}.meta`);
});
}
// ---- missing-coordinate fields must not yield NaN coordinates ----
test('workflow: node missing col never writes NaN', () => {
const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', EXAMPLES.workflow), 'utf8'));
delete doc.nodes[0].col;
assertFriendlyFailure('workflow', doc, 'workflow node no col');
});
test('lifecycle: state missing col never writes NaN', () => {
const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', EXAMPLES.lifecycle), 'utf8'));
delete doc.states[0].col;
assertFriendlyFailure('lifecycle', doc, 'lifecycle state no col');
});
// ---- property test: deterministic VALID perturbations always render ----
// Seeded PRNG (no Math.random — keeps the test reproducible across runs).
function mulberry32(seed) {
return function next() {
seed |= 0; seed = (seed + 0x6D2B79F5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
test('property: shuffling node/state order still renders (order-independence)', () => {
for (const mode of ['workflow', 'dataflow', 'lifecycle']) {
const arrKey = mode === 'lifecycle' ? 'states' : 'nodes';
for (let seed = 1; seed <= 8; seed += 1) {
const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', EXAMPLES[mode]), 'utf8'));
const rng = mulberry32(seed);
// FisherYates with the seeded PRNG.
const a = doc[arrKey];
for (let i = a.length - 1; i > 0; i -= 1) {
const j = Math.floor(rng() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
const { code, html } = render(mode, doc);
assert.equal(code, 0, `${mode} seed ${seed}: valid shuffle should render (exit 0)`);
assert.doesNotMatch(html, /NaN|undefined>/, `${mode} seed ${seed}: NaN in output`);
}
}
});
test('installed skill rejects unknown fields without node_modules', () => {
const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', EXAMPLES.workflow), 'utf8'));
doc.nodes[0].colour = 'cyan';
const { code, stderr } = render('workflow', doc);
assert.notEqual(code, 0);
assert.match(stderr, /workflow schema validation failed/);
assert.match(stderr, /\/nodes\/0 \(id\/label: "user"\) must NOT have additional properties/);
assert.match(stderr, /"additionalProperty":"colour"/);
assert.doesNotMatch(stderr, /ajv is not installed|skipping JSON-schema validation/);
});
for (const mode of Object.keys(EXAMPLES)) {
test(`installed skill retains full ${mode} schema without node_modules`, () => {
const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', EXAMPLES[mode]), 'utf8'));
doc.unknownField = true;
const { code, stderr } = render(mode, doc);
assert.notEqual(code, 0);
assert.match(stderr, new RegExp(`${mode} schema validation failed`));
assert.match(stderr, /"additionalProperty":"unknownField"/);
});
}
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,32 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import assert from 'node:assert/strict';
const here = path.dirname(fileURLToPath(import.meta.url));
const skill = readFileSync(path.join(here, '..', 'SKILL.md'), 'utf8');
const delivery = readFileSync(path.join(here, '..', 'references', 'delivery-contract.md'), 'utf8');
test('skill requires a bounded and truthful perceptual delivery receipt', () => {
assert.match(delivery, /visual_review: passed/);
assert.match(delivery, /visual_review: skipped \(image reader unavailable\)/);
assert.match(delivery, /correction_rounds: [0-2]/);
assert.match(delivery, /maximum of two focused correction rounds/i);
assert.match(delivery, /never report `visual_review: passed` without inspecting/i);
});
test('skill uses atomic verified delivery for the final artifact', () => {
assert.match(delivery, /archify\.mjs deliver <type>/);
assert.match(delivery, /same-directory candidate/i);
assert.match(delivery, /only replaces the target after.*artifact checks pass/i);
assert.match(delivery, /never claim that the deterministic receipt includes visual review/i);
});
test('skill keeps optional opening behind the verified commit and outside automation', () => {
assert.match(delivery, /Add `--open` only when the user wants an immediate local preview/);
assert.match(delivery, /runs after that atomic commit/);
assert.match(delivery, /Keep it off for CI, unattended agents, and non-interactive environments/);
assert.match(delivery, /never invokes an opener/);
assert.match(delivery, /status proves only whether the local opener invocation succeeded/);
});
@@ -0,0 +1,58 @@
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';
import { findChrome, runVisualCheck } from '../bin/visual-check.mjs';
import { DESKTOP_READABILITY_VIEWPORT, MIN_PROJECTED_NODE_TEXT_PX } from '../renderers/shared/desktop-readability.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const chromePath = process.env.ARCHIFY_CHROME ? findChrome() : null;
test('production showcase is readable in the real 1440 by 900 adaptive reader', {
skip: chromePath ? false : 'Set ARCHIFY_CHROME to run the real browser regression.',
}, async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-desktop-reader-'));
const artifact = path.join(tmp, 'production-deployment.html');
try {
execFileSync(process.execPath, [
path.join(skillRoot, 'bin', 'archify.mjs'),
'render',
'architecture',
path.join(skillRoot, 'examples', 'production-deployment.architecture.json'),
artifact,
'--quality',
'showcase',
], { cwd: skillRoot, encoding: 'utf8' });
for (let attempt = 1; attempt <= 3; attempt += 1) {
const result = await runVisualCheck({ artifactPath: artifact, chromePath });
assert.equal(result.exitCode, 0, `attempt ${attempt}: ${JSON.stringify(result.receipt, null, 2)}`);
assert.equal(result.receipt.readability.status, 'pass', `attempt ${attempt}: ${JSON.stringify(result.receipt, null, 2)}`);
const desktop = result.receipt.readability.viewports.find(({ width, height }) => (
width === DESKTOP_READABILITY_VIEWPORT.width && height === DESKTOP_READABILITY_VIEWPORT.height
));
const darkDesktop = result.receipt.captures.screenshots.find(({ width, height, theme }) => (
width === DESKTOP_READABILITY_VIEWPORT.width
&& height === DESKTOP_READABILITY_VIEWPORT.height
&& theme === 'dark'
));
for (const observation of [desktop, darkDesktop]) {
assert.ok(observation);
assert.equal(observation.readerWidth, 960);
assert.equal(observation.diagramWidth, 930);
assert.ok(observation.minimumProjectedNodeTextPx >= MIN_PROJECTED_NODE_TEXT_PX);
assert.equal(observation.minimumProjectedNodeTextDetail, 'boundary');
assert.equal(observation.minimumProjectedNodeText, 'AWS eu-west-1 / disaster recovery');
assert.equal(observation.readabilityOk, true);
assert.equal(observation.scrollHeight, DESKTOP_READABILITY_VIEWPORT.height);
}
}
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
@@ -0,0 +1,92 @@
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-diagram-guide-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all typed renderers inherit one viewer-only Diagram Guide', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /id="diagram-guide" hidden role="dialog" aria-modal="false" aria-labelledby="diagram-guide-title"/, mode);
assert.match(html, /id="btn-diagram-guide"[^>]+aria-label="Open diagram guide"[^>]+aria-haspopup="dialog"[^>]+aria-expanded="false"/, mode);
assert.match(html, /Archify\.guide = \(function \(\)/, mode);
assert.match(html, /Diagram Guide — a factual command deck over existing interactions/, mode);
assert.doesNotMatch(canonicalSvg(html), /diagram-guide|Archify\.guide|Explore this system/, mode);
}
});
test('Diagram Guide reports compiled semantic facts and honest story availability', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /svg\.querySelectorAll\('\[data-node-id\]'\)\.length/);
assert.match(html, /svg\.querySelectorAll\('\[data-edge-from\]\[data-edge-to\]'\)/);
assert.match(html, /edge\.getAttribute\('data-edge-key'\)/);
assert.match(html, /return Archify\.guidedViews && Number\(Archify\.guidedViews\.count\) \|\| 0/);
assert.match(html, /storyBtn\.disabled = views === 0/);
assert.match(html, /viewerCount\('viewer\.guide\.fact\.view', views\)/);
assert.match(html, /viewerText\('viewer\.guide\.story\.unavailable'\)/);
});
test('Diagram Guide delegates its task rows to existing production interactions', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /if \(action === 'find'\) return Archify\.finder\.open\(\)/);
assert.match(html, /if \(action === 'route'\) return Archify\.routeProbe\.begin\(\{ focusNode: true \}\)/);
assert.match(html, /if \(action === 'map'\) return Archify\.radar\.open\(\)/);
assert.match(html, /if \(action === 'story'\) return Archify\.guidedViews\.play\(\)/);
assert.match(html, /if \(action === 'present'\) return Archify\.presentation\.enter\(\)/);
assert.match(html, /if \(action === 'export'\) return Archify\.exportMenu\.open\(\)/);
assert.match(html, /if \(action === 'theme'\) return Archify\.theme\.toggle\(\)/);
assert.match(html, /if \(action === 'reset'\) return Archify\.view\.reset\(\)/);
assert.match(html, /Archify\.guidedViews\.pause\(\)/);
assert.match(html, /Archify\.finder\.close\(\{ restoreFocus: false \}\)/);
assert.match(html, /Archify\.radar\.close\(\{ restoreFocus: false \}\)/);
assert.match(html, /event\.stopPropagation\(\);[\s\S]+execute\(button\.getAttribute\('data-guide-action'\)\)/);
});
test('Diagram Guide is keyboard-first, mobile-contained, motion-safe, and embed-clean', () => {
const html = render('sequence', CASES.sequence);
assert.match(html, /e\.key === '\?'/);
assert.match(html, /Archify\.guide\.toggle\(\)/);
assert.match(html, /event\.key === 'ArrowRight'/);
assert.match(html, /event\.key === 'ArrowDown'/);
assert.match(html, /event\.key === 'Home'/);
assert.match(html, /event\.key === 'End'/);
assert.match(html, /event\.key === 'Escape' \|\| event\.key === '\?'/);
assert.match(html, /html\.setAttribute\('data-guide-open', 'true'\)/);
assert.match(html, /html\.getAttribute\('data-guide-open'\) === 'true'/);
assert.match(html, /html\[data-embed="true"\] \.diagram-guide/);
assert.match(html, /data-wide-diagram="true"\] \.diagram-guide/);
assert.match(html, /\.route-probe\[data-guide-open="true"\]/);
assert.match(html, /@media \(prefers-reduced-motion: reduce\)[\s\S]+\.diagram-guide/);
assert.match(html, /class="diagram-guide no-print"/);
assert.doesNotMatch(canonicalSvg(html), /data-guide-open|diagram-guide/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,209 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
deploymentOwnershipDiagnostics,
validateEngineeringProfile,
} from '../renderers/shared/engineering-profiles.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const cli = path.join(skillRoot, 'bin', 'archify.mjs');
const examplePath = path.join(skillRoot, 'examples', 'production-deployment.architecture.json');
const example = JSON.parse(fs.readFileSync(examplePath, 'utf8'));
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function validateJson(input, output) {
return spawnSync(process.execPath, [cli, 'validate', 'architecture', input, '--json'], {
cwd: path.dirname(output),
encoding: 'utf8',
});
}
test('deployment ownership profile passes the checked production example and stays opt-in', () => {
assert.equal(example.meta.engineering_profile, 'deployment-ownership');
assert.deepEqual(deploymentOwnershipDiagnostics(example), []);
assert.doesNotThrow(() => validateEngineeringProfile('architecture', example));
const ordinary = clone(example);
delete ordinary.meta.engineering_profile;
ordinary.boundaries = [];
ordinary.components.forEach((component) => { delete component.tag; });
assert.doesNotThrow(() => validateEngineeringProfile('architecture', ordinary));
});
test('deployment ownership profile reports exact owners, scopes, state, and crossing mechanisms', () => {
const candidate = clone(example);
delete candidate.components.find((component) => component.id === 'edge').tag;
candidate.boundaries.find((boundary) => boundary.label.includes('us-east-1')).wraps =
candidate.boundaries.find((boundary) => boundary.label.includes('us-east-1')).wraps.filter((id) => id !== 'edge');
candidate.boundaries.find((boundary) => boundary.label === 'private application network').wraps =
candidate.boundaries.find((boundary) => boundary.label === 'private application network').wraps.filter((id) => id !== 'redis');
const crossing = candidate.connections.find((connection) => connection.from === 'gateway' && connection.to === 'api_a');
crossing.label = '';
const diagnostics = deploymentOwnershipDiagnostics(candidate);
const codes = new Set(diagnostics.map((entry) => entry.code));
assert.ok(codes.has('engineering/deployment-owner-missing'));
assert.ok(codes.has('engineering/deployment-region-scope'));
assert.ok(codes.has('engineering/deployment-private-state'));
assert.ok(codes.has('engineering/deployment-crossing-mechanism'));
const boundaryDiagnostic = diagnostics.find((entry) => entry.code === 'engineering/deployment-crossing-mechanism');
assert.equal(boundaryDiagnostic.subject.collection, 'connections');
assert.equal(boundaryDiagnostic.evidence.from, 'gateway');
assert.equal(boundaryDiagnostic.evidence.to, 'api_a');
assert.ok(boundaryDiagnostic.evidence.crossedBoundaries.some((boundary) => boundary.kind === 'security-group'));
assert.deepEqual(boundaryDiagnostic.supportedFixes, [
`set /connections/${boundaryDiagnostic.subject.index}/label to the real cross-boundary mechanism`,
]);
});
test('deployment ownership profile requires both region and private boundary kinds', () => {
const candidate = clone(example);
candidate.boundaries = candidate.boundaries.filter((boundary) => boundary.kind === 'region');
const diagnostics = deploymentOwnershipDiagnostics(candidate);
assert.ok(diagnostics.some((entry) => entry.code === 'engineering/deployment-boundary-kind'
&& entry.evidence.requiredKind === 'security-group'));
});
test('deployment ownership profile rejects ambiguous regions and cross-region private groups', () => {
const candidate = clone(example);
const secondRegion = candidate.boundaries.find((boundary) => boundary.label.includes('eu-west-1'));
secondRegion.wraps.push('api_a');
const diagnostics = deploymentOwnershipDiagnostics(candidate);
assert.ok(diagnostics.some((entry) => entry.code === 'engineering/deployment-region-ambiguous'
&& entry.subject.id === 'api_a'));
assert.ok(diagnostics.some((entry) => entry.code === 'engineering/deployment-private-region-consistency'
&& entry.subject.collection === 'boundaries'));
});
test('deployment ownership crossing math follows authored membership instead of geometry or labels', () => {
const cases = [
['outside to region', 'clients', 'edge'],
['region to region', 'postgres', 'replica'],
['public to private', 'gateway', 'api_a'],
['private to public', 'worker', 'audit'],
];
for (const [name, from, to] of cases) {
const candidate = clone(example);
candidate.connections.forEach((connection) => {
connection.label ||= 'same-scope relation';
});
const connection = candidate.connections.find((entry) => entry.from === from && entry.to === to);
connection.label = '';
const crossings = deploymentOwnershipDiagnostics(candidate)
.filter((diagnostic) => diagnostic.code === 'engineering/deployment-crossing-mechanism');
assert.equal(crossings.length, 1, name);
assert.equal(crossings[0].evidence.from, from, name);
assert.equal(crossings[0].evidence.to, to, name);
}
const sameScope = clone(example);
sameScope.connections.forEach((connection) => {
connection.label ||= 'same-scope relation';
});
sameScope.connections.find((connection) => connection.from === 'api_a' && connection.to === 'redis').label = '';
sameScope.connections.push({ from: 'api_a', to: 'api_a', label: '' });
assert.ok(!deploymentOwnershipDiagnostics(sameScope)
.some((diagnostic) => diagnostic.code === 'engineering/deployment-crossing-mechanism'));
});
test('other diagram modes reject the architecture-only engineering profile', () => {
const fixtures = [
['workflow', 'agent-tool-call.workflow.json'],
['sequence', 'cache-miss-request.sequence.json'],
['dataflow', 'product-analytics.dataflow.json'],
['lifecycle', 'agent-run.lifecycle.json'],
];
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-engineering-schema-'));
try {
for (const [mode, fixture] of fixtures) {
const candidate = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', fixture), 'utf8'));
candidate.meta.engineering_profile = 'deployment-ownership';
const input = path.join(tmp, `${mode}.json`);
fs.writeFileSync(input, JSON.stringify(candidate));
const result = spawnSync(process.execPath, [cli, 'validate', mode, input, '--json'], {
cwd: tmp,
encoding: 'utf8',
});
assert.notEqual(result.status, 0, mode);
const receipt = JSON.parse(result.stdout);
assert.ok(receipt.diagnostics.some((diagnostic) => diagnostic.code === 'schema/additionalProperties'), mode);
}
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test('validate and deliver expose one truthful engineering-profile receipt', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-engineering-profile-'));
try {
const invalidPath = path.join(tmp, 'invalid.architecture.json');
const invalid = clone(example);
const crossing = invalid.connections.find((connection) => connection.from === 'gateway' && connection.to === 'api_a');
crossing.label = '';
fs.writeFileSync(invalidPath, JSON.stringify(invalid, null, 2));
const preservedOutput = path.join(tmp, 'preserved.html');
const preservedBytes = Buffer.from('last known good deployment');
fs.writeFileSync(preservedOutput, preservedBytes);
const failedDelivery = spawnSync(process.execPath, [
cli, 'deliver', 'architecture', invalidPath, preservedOutput, '--json',
], { cwd: tmp, encoding: 'utf8' });
assert.notEqual(failedDelivery.status, 0);
assert.equal(fs.readFileSync(preservedOutput).equals(preservedBytes), true);
const failed = validateJson(invalidPath, path.join(tmp, 'unused.html'));
assert.notEqual(failed.status, 0);
assert.equal(failed.stderr, '');
const failure = JSON.parse(failed.stdout);
assert.equal(failure.ok, false);
assert.equal(failure.stage, 'render');
assert.ok(failure.diagnostics.some((entry) => entry.code === 'engineering/deployment-crossing-mechanism'));
const validated = spawnSync(process.execPath, [cli, 'validate', 'architecture', examplePath, '--json'], {
cwd: tmp,
encoding: 'utf8',
});
assert.equal(validated.status, 0, validated.stderr);
assert.equal(JSON.parse(validated.stdout).engineeringProfile, 'deployment-ownership');
const output = path.join(tmp, 'deployment.html');
const delivered = spawnSync(process.execPath, [cli, 'deliver', 'architecture', examplePath, output, '--json'], {
cwd: tmp,
encoding: 'utf8',
});
assert.equal(delivered.status, 0, delivered.stderr);
const receipt = JSON.parse(delivered.stdout);
assert.equal(receipt.validation.engineeringProfile, 'deployment-ownership');
assert.match(fs.readFileSync(output, 'utf8'), /data-engineering-profile="deployment-ownership"/);
const secondOutput = path.join(tmp, 'deployment-second.html');
const repeated = spawnSync(process.execPath, [
cli, 'deliver', 'architecture', examplePath, secondOutput, '--json',
], { cwd: tmp, encoding: 'utf8' });
assert.equal(repeated.status, 0, repeated.stderr);
const digest = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
assert.equal(digest(output), digest(secondOutput));
const ordinaryInput = path.join(skillRoot, 'examples', 'web-app.architecture.json');
const ordinaryOutput = path.join(tmp, 'ordinary.html');
const ordinary = spawnSync(process.execPath, [
cli, 'render', 'architecture', ordinaryInput, ordinaryOutput,
], { cwd: tmp, encoding: 'utf8' });
assert.equal(ordinary.status, 0, ordinary.stderr);
assert.doesNotMatch(fs.readFileSync(ordinaryOutput, 'utf8'), /data-engineering-profile=/);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
+104
View File
@@ -0,0 +1,104 @@
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-finder-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function svg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all typed renderers ship the same geometry-neutral node finder', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /id="btn-node-finder"[^>]+aria-label="Find a node"[^>]+aria-haspopup="dialog"/, mode);
assert.match(html, /id="node-finder" hidden role="dialog" aria-modal="false"/, mode);
assert.match(html, /id="node-finder-input" type="search"/, mode);
assert.match(html, /Archify\.finder = \(function \(\)/, mode);
assert.match(html, /svg\.querySelectorAll\('\[data-node-id\]'\)/, mode);
assert.doesNotMatch(svg(html), /node-finder|Archify\.finder|Find a node/, mode);
}
});
test('finder searches semantic ids and labels, then delegates to focus and reveal', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /search: \(id \+ ' ' \+ label \+ ' ' \+ type \+ ' ' \+ sublabel \+ ' ' \+ context \+ ' ' \+ tag \+ ' ' \+ sourceSearch \+ ' ' \+ text\)\.toLowerCase\(\)/);
assert.match(html, /item\.search\.indexOf\(query\) !== -1/);
assert.match(html, /Archify\.guidedViews\.showAll\(\{ clearFocus: false, updateUrl: false \}\)/);
assert.match(html, /Archify\.view\.reset\(\{ automatic: true \}\)/);
assert.match(html, /Archify\.focus\.set\(id, \{ toggle: false \}\)/);
assert.match(html, /Archify\.view\.reveal\(\[id\], \{ includeNeighbors: true, reason: 'finder' \}\)/);
assert.match(html, /item\.node\.focus\(\{ preventScroll: true \}\)/);
assert.match(html, /var key = from \+ '\\u0000' \+ to/);
});
test('finder presents one focused search control and a structured result list', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /id="node-finder-input"[^>]+aria-label="Search diagram nodes"/);
assert.match(html, /\.node-finder-search:focus-within\s*\{/);
assert.match(html, /\.node-finder-input:focus-visible\s*\{\s*outline:\s*none;/);
assert.match(html, /\.node-finder\s*\{[\s\S]*?display:\s*flex;[\s\S]*?max-height:\s*calc\(100% - 2rem\);/);
assert.match(html, /\.node-finder-results\s*\{[\s\S]*?flex:\s*1 1 auto;[\s\S]*?min-height:\s*0;/);
assert.match(html, /\.node-finder-result:not\(:last-child\)\s*\{/);
assert.match(html, /context\.kind === 'focus'\s*\? viewerCount\('viewer\.finder\.link', item\.links\)/);
assert.match(html, /\[viewerKindLabel\(item\.type\), item\.id, item\.sublabel, item\.tag\]/);
assert.doesNotMatch(html, /\[item\.type, item\.context, item\.sublabel, item\.tag, item\.id\]/);
assert.match(html, /viewerText\('viewer\.finder\.status\.filtered'/);
});
test('finder becomes a contextual Route Probe endpoint picker without changing semantic focus', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /function resolveContext\(options\)/);
assert.match(html, /Archify\.routeProbe\.finderContext\(\)/);
assert.match(html, /context\.allowedIds\.indexOf\(item\.id\) !== -1/);
assert.match(html, /context\.kind === 'route-source' \|\| context\.kind === 'route-target'/);
assert.match(html, /Archify\.routeProbe\.choose\(id\)/);
assert.match(html, /reason: 'route-pick'/);
assert.match(html, /data-context="route-source"/);
assert.match(html, /data-context="route-target"/);
assert.match(html, /viewerText\('viewer\.finder\.result\.routeTarget'/);
assert.match(html, /links: badge/);
assert.match(html, /viewerText\('viewer\.finder\.status\.all'/);
});
test('finder is keyboard accessible, mobile-pinned, and subordinate to embed mode', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /e\.key === '\/'/);
assert.match(html, /Archify\.finder\.open\(\)/);
assert.match(html, /event\.key === 'ArrowDown'/);
assert.match(html, /event\.key === 'ArrowUp'/);
assert.match(html, /event\.key === 'Escape'/);
assert.match(html, /event\.stopPropagation\(\)/);
assert.match(html, /Archify\.exportMenu\.isOpen\(\)\) Archify\.exportMenu\.close\(false\)/);
assert.match(html, /data-wide-diagram="true"\] \.node-finder/);
assert.match(html, /html\[data-embed="true"\] \.node-finder/);
assert.match(html, /html\.getAttribute\('data-embed'\) === 'true'/);
assert.match(html, /data-node-finder-trigger/);
assert.match(html, /!event\.target\.closest\('\[data-node-finder-trigger\]'\)/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,40 @@
{
"schema_version": 1,
"diagram_type": "workflow",
"meta": {
"title": "自动连线节点边框避让回归",
"quality_profile": "showcase",
"viewBox": [720, 360]
},
"lanes": [
{ "id": "orchestrator", "label": "Orchestrator PTY" },
{ "id": "runtime", "label": "Hive Runtime" }
],
"nodes": [
{
"id": "team_send",
"lane": "orchestrator",
"col": 2,
"type": "backend",
"label": "team send 派单",
"sublabel": "选择目标 Worker"
},
{
"id": "inject",
"lane": "runtime",
"col": 1,
"type": "security",
"label": "校验并注入输入",
"sublabel": "UI token → PTY stdin"
}
],
"edges": [
{
"id": "stdin",
"from": "inject",
"to": "team_send",
"label": "PTY stdin",
"variant": "emphasis"
}
]
}
@@ -0,0 +1,17 @@
import fs from 'node:fs';
import path from 'node:path';
const originalRmSync = fs.rmSync.bind(fs);
let injectedFailure = false;
fs.rmSync = function failMigrationCleanupOnce(target, options) {
const isMigrationStagingDirectory = path.basename(String(target)).startsWith('.archify-migration-');
if (!injectedFailure && isMigrationStagingDirectory) {
injectedFailure = true;
originalRmSync(target, options);
const error = new Error('simulated migration cleanup failure');
error.code = 'EPERM';
throw error;
}
return originalRmSync(target, options);
};
@@ -0,0 +1,18 @@
{
"schema_version": 2,
"diagram_type": "workflow",
"meta": {
"title": "Sanitized custom widths",
"legend": { "mode": "hidden" }
},
"lanes": [
{ "id": "main", "label": "Main flow" }
],
"nodes": [
{ "id": "compact", "lane": "main", "col": 1, "type": "frontend", "label": "In", "width": 32 },
{ "id": "wide", "lane": "main", "col": 2, "type": "database", "label": "Sanitized downstream service", "width": 240 }
],
"edges": [
{ "id": "compact-wide", "from": "compact", "to": "wide" }
]
}
@@ -0,0 +1,28 @@
{
"schema_version": 2,
"diagram_type": "workflow",
"meta": {
"title": "Sanitized explicit route",
"viewBox": [900, 420],
"legend": { "mode": "hidden" }
},
"lanes": [
{ "id": "source", "label": "Source" },
{ "id": "target", "label": "Target" }
],
"nodes": [
{ "id": "producer", "lane": "source", "col": 1, "type": "backend", "label": "Producer" },
{ "id": "consumer", "lane": "target", "col": 1, "type": "backend", "label": "Consumer" }
],
"edges": [
{
"id": "producer-consumer",
"from": "producer",
"to": "consumer",
"fromSide": "right",
"toSide": "right",
"route": "outside-right",
"channelX": 720
}
]
}
@@ -0,0 +1,19 @@
{
"schema_version": 2,
"diagram_type": "workflow",
"meta": {
"title": "Sanitized explicit viewBox",
"viewBox": [1600, 520],
"legend": { "mode": "hidden" }
},
"lanes": [
{ "id": "main", "label": "Main flow" }
],
"nodes": [
{ "id": "start", "lane": "main", "col": 0, "type": "frontend", "label": "Start" },
{ "id": "finish", "lane": "main", "col": 5, "type": "backend", "label": "Finish" }
],
"edges": [
{ "id": "start-finish", "from": "start", "to": "finish", "label": "complete" }
]
}
@@ -0,0 +1,18 @@
{
"schema_version": 2,
"diagram_type": "workflow",
"meta": {
"title": "Sanitized semantic labels",
"legend": { "mode": "hidden" }
},
"lanes": [
{ "id": "main", "label": "Main flow" }
],
"nodes": [
{ "id": "request", "lane": "main", "col": 3, "type": "frontend", "label": "Request" },
{ "id": "result", "lane": "main", "col": 4, "type": "backend", "label": "Result" }
],
"edges": [
{ "id": "request-result", "from": "request", "to": "result", "label": "同步 ✅ ready" }
]
}
@@ -0,0 +1,64 @@
{
"schema_version": 1,
"diagram_type": "lifecycle",
"meta": {
"title": "Agent Run Lifecycle",
"subtitle": "State machine for planning, tool execution, human approval, retries, and terminal outcomes",
"output": "examples/lifecycle-agent-run.html",
"viewBox": [980, 660]
},
"lanes": [
{ "id": "main", "label": "Lifecycle phases" },
{ "id": "waiting", "label": "Interruptions" },
{ "id": "exceptions", "label": "Recovery loop" },
{ "id": "terminal", "label": "Terminal exits" }
],
"states": [
{ "id": "queued", "type": "start", "label": "Queued", "sublabel": "request accepted", "lane": "main", "col": 0, "step": "01", "tag": "entry" },
{ "id": "planning", "type": "active", "label": "Planning", "sublabel": "build task graph", "lane": "main", "col": 1, "step": "02", "tag": "model" },
{ "id": "executing", "type": "active", "label": "Executing", "sublabel": "tool calls", "lane": "main", "col": 2, "step": "03", "tag": "work" },
{ "id": "reviewing", "type": "decision", "label": "Reviewing", "sublabel": "quality gate", "lane": "main", "col": 3, "step": "04", "tag": "check" },
{ "id": "completed", "type": "success", "label": "Completed", "sublabel": "final response", "lane": "main", "col": 4, "step": "05", "tag": "done" },
{ "id": "approval", "type": "waiting", "label": "Needs Approval", "sublabel": "human gate", "lane": "waiting", "col": 0, "tag": "pause" },
{ "id": "blocked", "type": "waiting", "label": "Blocked", "sublabel": "missing input", "lane": "waiting", "col": 1, "tag": "wait" },
{ "id": "failed", "type": "failure", "label": "Failed", "sublabel": "recoverable error", "lane": "exceptions", "col": 0, "yOffset": 78, "tag": "retryable" },
{ "id": "cancelled", "type": "failure", "label": "Cancelled", "sublabel": "user stopped", "lane": "terminal", "col": 0, "tag": "terminal" },
{ "id": "expired", "type": "failure", "label": "Expired", "sublabel": "timeout", "lane": "terminal", "col": 1, "tag": "terminal" }
],
"transitions": [
{ "from": "executing", "to": "approval", "variant": "security", "fromSide": "bottom", "toSide": "top", "route": "straight" },
{ "from": "reviewing", "to": "blocked", "variant": "default", "route": "drop" },
{ "from": "executing", "to": "failed", "variant": "security", "fromSide": "left", "toSide": "top", "via": [[320, 157], [320, 342], [402, 342]] },
{ "from": "blocked", "to": "expired", "variant": "security", "fromSide": "bottom", "toSide": "top", "route": "straight" },
{ "from": "approval", "to": "cancelled", "variant": "security", "fromSide": "bottom", "toSide": "top", "via": [[320, 336], [320, 430], [402, 430]] }
],
"cards": [
{
"dot": "emerald",
"title": "Main Path",
"items": [
"The run has five ordered phases from queue to completion",
"The primary lifecycle is carried by one horizontal rail",
"Completion is a phase, not a detached side box"
]
},
{
"dot": "amber",
"title": "Human + Input Gates",
"items": [
"Approval pauses execution without ending the run",
"Blocked waits for missing user input",
"Wait states can resume back into planning or execution"
]
},
{
"dot": "rose",
"title": "Terminal + Recovery",
"items": [
"Failed loops back while retry budget remains",
"Cancelled and Expired are exits from the lifecycle",
"Terminal exits do not point back into active execution"
]
}
]
}
@@ -0,0 +1,88 @@
{
"schema_version": 1,
"diagram_type": "workflow",
"meta": {
"title": "Agent Tool Call Workflow",
"subtitle": "Renderer-driven workflow prototype with lanes, anchored nodes, and orthogonal edges",
"output": "examples/workflow-agent-tool-call-rendered.html",
"viewBox": [720, 900]
},
"lanes": [
{ "id": "ui", "label": "User Interface" },
{ "id": "agent", "label": "Agent Runtime" },
{ "id": "policy", "label": "Policy Boundary" },
{ "id": "exceptions", "label": "Exception Handling", "variant": "exception" },
{ "id": "tools", "label": "Tool Execution" },
{ "id": "trace", "label": "Observability" }
],
"phases": [
{ "id": "intake", "label": "Intake", "fromCol": 0, "toCol": 1 },
{ "id": "reasoning", "label": "Plan + route", "fromCol": 2, "toCol": 3, "variant": "emphasis" },
{ "id": "execution", "label": "Execute + report", "fromCol": 4, "toCol": 5, "variant": "dashed" }
],
"groups": [
{ "id": "agent_loop", "label": "Planning loop", "lane": "agent", "fromCol": 2, "toCol": 3, "variant": "emphasis" },
{ "id": "tool_work", "label": "Tool work", "lane": "tools", "fromCol": 4, "toCol": 5, "variant": "dashed" },
{ "id": "exception_path", "label": "Human or policy stop", "lane": "exceptions", "fromCol": 3, "toCol": 5, "variant": "security" }
],
"mainPath": ["user", "chat", "planner", "router", "approval", "tool", "external", "final"],
"nodes": [
{ "id": "user", "lane": "ui", "col": 0, "type": "external", "label": "User", "sublabel": "asks for work" },
{ "id": "chat", "lane": "ui", "col": 1, "type": "frontend", "label": "Chat Surface", "sublabel": "thread + files" },
{ "id": "final", "lane": "ui", "col": 5, "type": "backend", "label": "Final Reply", "sublabel": "answer + changes" },
{ "id": "planner", "lane": "agent", "col": 2, "type": "backend", "label": "Agent Planner", "sublabel": "plan next step", "tag": "context aware" },
{ "id": "router", "lane": "agent", "col": 3, "type": "backend", "label": "Tool Router", "sublabel": "choose capability" },
{ "id": "approval", "lane": "policy", "col": 3, "type": "security", "label": "Approval Gate", "sublabel": "scope + consent", "tag": "block risky ops" },
{ "id": "blocked", "lane": "exceptions", "col": 4, "type": "security", "label": "Blocked", "sublabel": "wait or reject" },
{ "id": "retry", "lane": "exceptions", "col": 5, "type": "messagebus", "label": "Retry Path", "sublabel": "revise request" },
{ "id": "tool", "lane": "tools", "col": 4, "type": "messagebus", "label": "Tool Call", "sublabel": "shell / browser / MCP", "tag": "structured result" },
{ "id": "external", "lane": "tools", "col": 5, "type": "cloud", "label": "External API", "sublabel": "network service" },
{ "id": "store", "lane": "trace", "col": 1, "type": "database", "label": "Context Store", "sublabel": "repo + memory" },
{ "id": "trace", "lane": "trace", "col": 4, "type": "database", "label": "Trace Log", "sublabel": "events + output" }
],
"edges": [
{ "from": "user", "to": "chat", "variant": "default" },
{ "from": "chat", "to": "planner", "label": "plan", "variant": "emphasis", "fromSide": "bottom", "toSide": "top", "route": "drop", "labelSegment": 1 },
{ "from": "planner", "to": "router", "variant": "default" },
{ "from": "router", "to": "approval", "label": "needs approval?", "variant": "security", "fromSide": "bottom", "toSide": "top", "route": "drop", "labelSegment": 0, "labelDx": 34, "labelDy": 18 },
{ "from": "approval", "to": "tool", "variant": "emphasis", "fromSide": "left", "toSide": "left", "route": "return-left" },
{ "from": "approval", "to": "blocked", "label": "denied", "variant": "security", "role": "error", "fromSide": "bottom", "toSide": "top", "route": "drop", "labelSegment": 1, "labelDy": 12 },
{ "from": "blocked", "to": "retry", "variant": "dashed", "role": "branch" },
{ "from": "tool", "to": "external", "variant": "default" },
{ "from": "external", "to": "final", "variant": "emphasis", "role": "return", "fromSide": "right", "toSide": "right", "route": "outside-right", "width": 1.2 },
{ "from": "external", "to": "trace", "label": "record result", "variant": "dashed", "fromSide": "bottom", "toSide": "bottom", "route": "bottom-channel", "labelSegment": 1 },
{ "from": "store", "to": "trace", "label": "trace + memory", "variant": "dashed", "labelAt": [365, 735] }
],
"cards": [
{
"dot": "cyan",
"title": "Renderer Rules",
"items": [
"Lanes and columns determine node placement",
"Edges attach to explicit node anchors",
"Cross-lane paths use orthogonal routing",
"Short adjacent links stay unlabeled"
]
},
{
"dot": "rose",
"title": "Workflow Semantics",
"items": [
"Approval is a first-class policy step",
"Consent gates are visible in the main path",
"External calls stay inside the tool lane",
"Trace writes are separate from the hot path"
]
},
{
"dot": "emerald",
"title": "Why It Matters",
"items": [
"This is closer to a diagram_type renderer",
"The graph can be edited without SVG surgery",
"Layout rules can be tested and improved",
"A future IR can reuse this shape directly"
]
}
]
}
@@ -0,0 +1,75 @@
{
"schema_version": 1,
"diagram_type": "sequence",
"meta": {
"title": "Cache Miss Request Sequence",
"subtitle": "Frontend request path with auth, cache fallback, persistence, and async trace",
"output": "examples/sequence-cache-miss-request.html",
"viewBox": [820, 760]
},
"participants": [
{ "id": "user", "type": "external", "label": "User", "sublabel": "browser session" },
{ "id": "web", "type": "frontend", "label": "Web App", "sublabel": "React UI" },
{ "id": "api", "type": "backend", "label": "API", "sublabel": "request handler" },
{ "id": "auth", "type": "security", "label": "Auth", "sublabel": "JWT verify" },
{ "id": "redis", "type": "database", "label": "Redis", "sublabel": "cache" },
{ "id": "db", "type": "database", "label": "Postgres", "sublabel": "source of truth" },
{ "id": "trace", "type": "messagebus", "label": "Trace", "sublabel": "async event" }
],
"segments": [
{ "from": 150, "to": 295, "label": "Request" },
{ "from": 315, "to": 505, "label": "Fallback" },
{ "from": 525, "to": 665, "label": "Response + trace" }
],
"messages": [
{ "from": "user", "to": "web", "y": 185, "label": "open page", "variant": "default" },
{ "from": "web", "to": "api", "y": 228, "label": "GET /dashboard", "variant": "emphasis" },
{ "from": "api", "to": "auth", "y": 270, "label": "verify JWT", "variant": "security" },
{ "from": "auth", "to": "api", "y": 305, "label": "claims ok", "variant": "return" },
{ "from": "api", "to": "redis", "y": 354, "label": "read cache", "variant": "default" },
{ "from": "redis", "to": "api", "y": 391, "label": "miss", "variant": "return" },
{ "from": "api", "to": "db", "y": 443, "label": "query profile + metrics", "variant": "emphasis" },
{ "from": "db", "to": "api", "y": 489, "label": "rows", "variant": "return" },
{ "from": "api", "to": "redis", "y": 536, "label": "set cache", "variant": "dashed" },
{ "from": "api", "to": "trace", "y": 580, "label": "emit trace", "variant": "dashed" },
{ "from": "api", "to": "web", "y": 625, "label": "200 JSON", "variant": "return" },
{ "from": "web", "to": "user", "y": 662, "label": "render", "variant": "return" }
],
"activations": [
{ "participant": "web", "from": 220, "to": 668, "type": "frontend" },
{ "participant": "api", "from": 228, "to": 632, "type": "backend" },
{ "participant": "auth", "from": 265, "to": 310, "type": "security" },
{ "participant": "redis", "from": 349, "to": 398, "type": "database" },
{ "participant": "db", "from": 438, "to": 496, "type": "database" },
{ "participant": "trace", "from": 575, "to": 630, "type": "messagebus" }
],
"cards": [
{
"dot": "emerald",
"title": "Happy Path",
"items": [
"The main request is Web App -> API -> data source -> response",
"Return messages are quieter than forward calls",
"Activation bars make ownership duration visible"
]
},
{
"dot": "rose",
"title": "Policy + Fallback",
"items": [
"JWT verification is colored as a security interaction",
"Cache miss is visible without overpowering the main path",
"Database access only appears after cache fallback"
]
},
{
"dot": "orange",
"title": "Async Trace",
"items": [
"Trace emission is dashed and secondary",
"It does not block the response path",
"The diagram separates user-facing latency from observability"
]
}
]
}
@@ -0,0 +1,57 @@
{
"schema_version": 1,
"diagram_type": "dataflow",
"meta": {
"title": "Order Event-stream Topology",
"output": "examples/event-stream.html",
"viewBox": [1080, 780],
"animation": "trace",
"visual_preset": "signal-flow",
"quality_profile": "showcase",
"views": [
{ "id": "order-transit", "label": "Order event transit", "focus": ["checkout", "orders", "validate", "state", "fulfillment"], "note": "Follow an order from producer through ordered processing to fulfillment." },
{ "id": "payment-transit", "label": "Payment event transit", "focus": ["billing", "payments", "enrich", "state", "analytics"], "note": "Track payment facts into the shared materialized state and analytics." },
{ "id": "failure-and-replay", "label": "Failure and replay", "focus": ["validate", "enrich", "dlq", "replay", "ops"], "note": "Isolate dead letters, operator review, and controlled replay ownership." }
]
},
"stages": [
{ "label": "Producers" },
{ "label": "Transit" },
{ "label": "Processors" },
{ "label": "State + recovery" },
{ "label": "Consumers" }
],
"nodes": [
{ "id": "checkout", "type": "frontend", "label": "Checkout API", "sublabel": "order producer", "stage": 0, "row": 0, "tag": "team commerce" },
{ "id": "billing", "type": "backend", "label": "Billing API", "sublabel": "payment producer", "stage": 0, "row": 2, "tag": "team money" },
{ "id": "orders", "type": "messagebus", "label": "orders.v1", "sublabel": "12 partitions", "stage": 1, "row": 0, "tag": "key: order_id" },
{ "id": "payments", "type": "messagebus", "label": "payments.v2", "sublabel": "8 partitions", "stage": 1, "row": 2, "tag": "key: order_id" },
{ "id": "validate", "type": "backend", "label": "Order Validate", "sublabel": "group fulfillment", "stage": 2, "row": 0, "tag": "ordered" },
{ "id": "enrich", "type": "backend", "label": "Payment Enrich", "sublabel": "group analytics", "stage": 2, "row": 2, "tag": "at-least-once" },
{ "id": "state", "type": "database", "label": "Order State", "sublabel": "materialized view", "stage": 3, "row": 1, "tag": "idempotent" },
{ "id": "dlq", "type": "messagebus", "label": "events.dlq", "sublabel": "poison events", "stage": 3, "row": 4, "tag": "7-day retention" },
{ "id": "fulfillment", "type": "backend", "label": "Fulfillment", "sublabel": "shipping workflow", "stage": 4, "row": 0, "tag": "consumer" },
{ "id": "analytics", "type": "database", "label": "Analytics", "sublabel": "streaming facts", "stage": 4, "row": 2, "tag": "consumer" },
{ "id": "replay", "type": "security", "label": "Replay Tool", "sublabel": "approved batch", "stage": 4, "row": 4, "tag": "operator gate" },
{ "id": "ops", "type": "external", "label": "On-call", "sublabel": "DLQ owner", "stage": 4, "row": 3, "yOffset": -18, "tag": "SRE" }
],
"flows": [
{ "from": "checkout", "to": "orders", "label": "OrderPlaced", "classification": "schema v1", "variant": "emphasis", "route": "straight" },
{ "from": "billing", "to": "payments", "label": "PaymentCaptured", "classification": "schema v2", "variant": "emphasis", "route": "straight" },
{ "from": "orders", "to": "validate", "label": "ordered orders", "classification": "consumer group", "variant": "emphasis", "route": "straight" },
{ "from": "payments", "to": "enrich", "label": "payment facts", "classification": "at-least-once", "variant": "emphasis", "route": "straight" },
{ "from": "validate", "to": "state", "label": "valid order", "classification": "idempotent", "variant": "emphasis", "route": "vertical-channel" },
{ "from": "enrich", "to": "state", "label": "enriched payment", "classification": "idempotent", "variant": "default", "route": "vertical-channel" },
{ "from": "state", "to": "fulfillment", "label": "ready orders", "classification": "read model", "variant": "emphasis", "route": "vertical-channel" },
{ "from": "state", "to": "analytics", "label": "order facts", "classification": "non-PII", "variant": "default", "route": "vertical-channel" },
{ "from": "validate", "to": "dlq", "label": "invalid event", "classification": "dead letter", "variant": "security", "fromSide": "top", "toSide": "top", "via": [[530, 80], [20, 80], [20, 550], [745, 550]], "labelAt": [300, 550] },
{ "from": "enrich", "to": "dlq", "label": "poison event", "classification": "dead letter", "variant": "security", "route": "bottom-channel", "labelDy": 30 },
{ "from": "dlq", "to": "ops", "label": "failure sample", "classification": "restricted", "variant": "security", "route": "vertical-channel" },
{ "from": "dlq", "to": "replay", "label": "approved replay", "classification": "audited batch", "variant": "dashed", "route": "straight", "labelDy": 30 }
],
"cards": [
{ "dot": "amber", "title": "Transit Contract", "items": ["Every event and topic is named", "Partition keys preserve per-order ordering", "Consumer groups expose processing ownership"] },
{ "dot": "emerald", "title": "State + Delivery", "items": ["Processors write an idempotent materialized view", "Fulfillment and analytics consume distinct assets", "At-least-once delivery never implies duplicate business effects"] },
{ "dot": "rose", "title": "Failure Ownership", "items": ["Poison events land in a retained dead-letter topic", "On-call inspects samples before replay", "Replay is gated, batched, and auditable"] }
]
}
@@ -0,0 +1,70 @@
{
"schema_version": 1,
"diagram_type": "dataflow",
"meta": {
"title": "Product Analytics Data Flow",
"subtitle": "Events, consent, PII isolation, warehouse sync, and downstream analytics",
"output": "examples/dataflow-product-analytics.html",
"viewBox": [1080, 760]
},
"stages": [
{ "label": "Sources" },
{ "label": "Ingest" },
{ "label": "Process" },
{ "label": "Store" },
{ "label": "Consume" }
],
"nodes": [
{ "id": "web", "type": "frontend", "label": "Web App", "sublabel": "browser SDK", "stage": 0, "row": 0, "tag": "events" },
{ "id": "mobile", "type": "frontend", "label": "Mobile", "sublabel": "iOS / Android", "stage": 0, "row": 2, "tag": "events" },
{ "id": "edge", "type": "cloud", "label": "Edge API", "sublabel": "collector", "stage": 1, "row": 1, "tag": "TLS" },
{ "id": "consent", "type": "security", "label": "Consent Gate", "sublabel": "policy filter", "stage": 2, "row": 0, "tag": "PII guard" },
{ "id": "stream", "type": "messagebus", "label": "Event Stream", "sublabel": "Kafka topic", "stage": 2, "row": 2, "tag": "ordered" },
{ "id": "pii", "type": "security", "label": "PII Vault", "sublabel": "encrypted", "stage": 3, "row": 0, "tag": "restricted" },
{ "id": "warehouse", "type": "database", "label": "Warehouse", "sublabel": "analytics tables", "stage": 3, "row": 2, "tag": "curated" },
{ "id": "features", "type": "database", "label": "Feature Store", "sublabel": "daily batch", "stage": 3, "row": 4, "tag": "derived" },
{ "id": "dashboard", "type": "backend", "label": "Dashboards", "sublabel": "product metrics", "stage": 4, "row": 1, "tag": "SQL" },
{ "id": "model", "type": "backend", "label": "ML Model", "sublabel": "ranking job", "stage": 4, "row": 4, "tag": "features" }
],
"flows": [
{ "from": "web", "to": "edge", "label": "clickstream", "classification": "user events", "variant": "emphasis", "fromSide": "right", "toSide": "left", "via": [[184, 157], [184, 271]], "labelAt": [204, 190] },
{ "from": "mobile", "to": "edge", "label": "app events", "classification": "device events", "variant": "default", "fromSide": "right", "toSide": "left", "via": [[222, 385], [222, 271]], "labelAt": [220, 342] },
{ "from": "edge", "to": "consent", "label": "identity + consent", "classification": "PII touch", "variant": "security", "fromSide": "top", "toSide": "left", "via": [[315, 112], [450, 112], [450, 157]], "labelAt": [382, 100] },
{ "from": "edge", "to": "stream", "label": "accepted events", "classification": "append-only", "variant": "emphasis", "fromSide": "right", "toSide": "left", "via": [[420, 271], [420, 385]], "labelAt": [438, 324] },
{ "from": "consent", "to": "pii", "label": "identity map", "classification": "encrypted PII", "variant": "security", "route": "straight", "labelAt": [638, 144] },
{ "from": "stream", "to": "warehouse", "label": "normalized facts", "classification": "non-PII", "variant": "emphasis", "route": "straight", "labelAt": [638, 372] },
{ "from": "warehouse", "to": "features", "label": "daily aggregates", "classification": "batch", "variant": "dashed", "fromSide": "bottom", "toSide": "top", "route": "straight", "labelAt": [745, 496] },
{ "from": "warehouse", "to": "dashboard", "label": "metrics SQL", "classification": "read-only", "variant": "default", "fromSide": "right", "toSide": "left", "via": [[852, 385], [852, 271]], "labelAt": [830, 326] },
{ "from": "features", "to": "model", "label": "feature vectors", "classification": "derived", "variant": "dashed", "route": "straight", "labelAt": [852, 598] },
{ "from": "pii", "to": "dashboard", "label": "restricted join", "classification": "approved only", "variant": "security", "fromSide": "right", "toSide": "top", "via": [[878, 157], [878, 212], [960, 212]], "labelAt": [880, 198] }
],
"cards": [
{
"dot": "emerald",
"title": "Primary Data Path",
"items": [
"Events move left to right through source, ingest, process, store, and consume stages",
"The hot path stays visually clear even with secondary batch flows",
"Labels name data assets instead of generic API verbs"
]
},
{
"dot": "rose",
"title": "Sensitive Boundary",
"items": [
"Consent and PII paths are styled as security flows",
"PII lands in a restricted vault, separate from the analytics warehouse",
"Restricted joins are visible without implying default access"
]
},
{
"dot": "orange",
"title": "Derived Consumers",
"items": [
"Dashboards read curated facts from the warehouse",
"Feature vectors are derived by batch from analytics tables",
"Consumption paths stay distinct from collection and consent handling"
]
}
]
}
@@ -0,0 +1,71 @@
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": {
"title": "Production Deployment Ownership",
"output": "examples/production-deployment.html",
"visual_preset": "blueprint",
"animation": "trace",
"quality_profile": "showcase",
"engineering_profile": "deployment-ownership",
"views": [
{
"id": "request-boundary",
"label": "Request crosses the edge",
"focus": ["clients", "edge", "gateway", "api_a", "api_b"],
"note": "Follow public traffic into the private application network."
},
{
"id": "state-ownership",
"label": "State and ownership",
"focus": ["api_a", "api_b", "redis", "postgres", "replica"],
"note": "Separate stateless platform workloads from data-team-owned state."
},
{
"id": "async-operations",
"label": "Async and operations",
"focus": ["api_b", "events", "worker", "audit", "observability"],
"note": "See the asynchronous work and the evidence it emits."
}
]
},
"components": [
{ "id": "clients", "type": "external", "label": "Customers", "sublabel": "web + mobile", "pos": [38, 300], "size": [122, 60] },
{ "id": "edge", "type": "cloud", "label": "Global Edge", "sublabel": "CDN + WAF", "pos": [230, 300], "size": [126, 60], "tag": "edge team" },
{ "id": "gateway", "type": "security", "label": "API Gateway", "sublabel": "public :443", "pos": [430, 300], "size": [128, 60], "tag": "platform" },
{ "id": "api_a", "type": "backend", "label": "API Pods / AZ-a", "sublabel": "private subnet", "pos": [630, 195], "size": [136, 62], "tag": "app team" },
{ "id": "api_b", "type": "backend", "label": "API Pods / AZ-b", "sublabel": "private subnet", "pos": [630, 405], "size": [136, 62], "tag": "app team" },
{ "id": "redis", "type": "database", "label": "Redis", "sublabel": "multi-AZ cache", "pos": [840, 195], "size": [126, 62], "tag": "platform" },
{ "id": "postgres", "type": "database", "label": "PostgreSQL", "sublabel": "primary / encrypted", "pos": [840, 405], "size": [126, 62], "tag": "data team" },
{ "id": "events", "type": "messagebus", "label": "Event Bus", "sublabel": "orders.v1", "pos": [1040, 300], "size": [126, 60], "tag": "platform" },
{ "id": "worker", "type": "backend", "label": "Workers", "sublabel": "private workload", "pos": [1240, 300], "size": [126, 60], "tag": "app team" },
{ "id": "replica", "type": "database", "label": "DR Replica", "sublabel": "eu-west-1", "pos": [1040, 610], "size": [126, 62], "tag": "data team" },
{ "id": "audit", "type": "cloud", "label": "Audit Archive", "sublabel": "immutable objects", "pos": [1240, 465], "size": [126, 62], "tag": "security" },
{ "id": "observability", "type": "external", "label": "Observability", "sublabel": "metrics + traces", "pos": [1240, 85], "size": [126, 62], "tag": "SRE" }
],
"boundaries": [
{ "kind": "region", "label": "AWS us-east-1 / production", "wraps": ["edge", "gateway", "api_a", "api_b", "redis", "postgres", "events", "worker", "audit"] },
{ "kind": "security-group", "label": "private application network", "wraps": ["api_a", "api_b", "redis", "postgres", "events", "worker"] },
{ "kind": "region", "label": "AWS eu-west-1 / disaster recovery", "wraps": ["replica"] },
{ "kind": "security-group", "label": "DR private subnet", "wraps": ["replica"], "pad": 14 }
],
"connections": [
{ "from": "clients", "to": "edge", "label": "HTTPS", "variant": "emphasis" },
{ "from": "edge", "to": "gateway", "label": "mTLS", "variant": "security" },
{ "from": "gateway", "to": "api_a", "label": "VPC route", "variant": "emphasis", "route": "orthogonal-h", "labelAt": [594, 275] },
{ "from": "gateway", "to": "api_b", "label": "VPC route", "variant": "emphasis", "route": "orthogonal-h", "labelAt": [594, 385] },
{ "from": "api_a", "to": "redis", "label": "cache", "route": "straight" },
{ "from": "api_b", "to": "postgres", "label": "SQL", "route": "straight" },
{ "from": "api_a", "to": "events", "label": "publish", "variant": "dashed", "fromSide": "top", "toSide": "top", "via": [[698, 170], [1103, 170]] },
{ "from": "api_b", "to": "events", "variant": "dashed", "fromSide": "top", "toSide": "bottom", "via": [[698, 380], [1103, 380]] },
{ "from": "events", "to": "worker", "variant": "emphasis" },
{ "from": "postgres", "to": "replica", "label": "cross-region WAL", "variant": "security", "route": "orthogonal-v", "labelAt": [1003, 529] },
{ "from": "worker", "to": "audit", "label": "evidence", "variant": "dashed", "fromSide": "bottom", "toSide": "top", "labelDy": 58 },
{ "from": "worker", "to": "observability", "label": "OTLP", "variant": "dashed", "route": "orthogonal-v" }
],
"cards": [
{ "dot": "cyan", "title": "Runtime Ownership", "items": ["Platform owns the edge, gateway, cache, and event bus", "Application teams own API pods and workers", "Data owns primary and disaster-recovery state"] },
{ "dot": "rose", "title": "Named Crossings", "items": ["Public HTTPS terminates at the managed edge", "mTLS crosses into the application network", "Cross-region WAL is explicit and encrypted"] },
{ "dot": "emerald", "title": "Operational Evidence", "items": ["Workers emit traces to SRE-owned observability", "Audit evidence lands in immutable storage", "Unknown placement should remain marked, never invented"] }
]
}
@@ -0,0 +1,41 @@
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": {
"title": "Sample Web App",
"subtitle": "Classic 3-tier SaaS on AWS — rendered by Archify",
"output": "web-app-rendered.html"
},
"components": [
{ "id": "users", "type": "external", "label": "Users", "sublabel": "Browser / Mobile", "pos": [40, 300], "size": [120, 60] },
{ "id": "auth", "type": "security", "label": "Auth Provider", "sublabel": "OAuth 2.0", "pos": [40, 110], "size": [120, 64], "tag": "JWT + PKCE" },
{ "id": "cdn", "type": "cloud", "label": "CloudFront", "sublabel": "CDN", "pos": [250, 300], "size": [130, 60] },
{ "id": "lb", "type": "cloud", "label": "Load Balancer", "sublabel": "HTTPS :443", "pos": [460, 300], "size": [130, 60] },
{ "id": "api", "type": "backend", "label": "API Server", "sublabel": "FastAPI :8000", "pos": [670, 300], "size": [130, 60] },
{ "id": "cache", "type": "database", "label": "Redis", "sublabel": "cache :6379", "pos": [670, 150], "size": [130, 60] },
{ "id": "db", "type": "database", "label": "PostgreSQL", "sublabel": "primary :5432", "pos": [880, 300], "size": [130, 60] },
{ "id": "s3", "type": "cloud", "label": "S3", "sublabel": "static assets", "pos": [250, 440], "size": [130, 60], "tag": "OAI protected" },
{ "id": "queue", "type": "messagebus", "label": "SQS", "sublabel": "job queue", "pos": [670, 440], "size": [130, 60] },
{ "id": "worker", "type": "backend", "label": "Worker", "sublabel": "async jobs", "pos": [880, 440], "size": [130, 60] }
],
"boundaries": [
{ "kind": "region", "label": "AWS Region: us-west-2", "wraps": ["cdn", "lb", "api", "cache", "db", "s3", "queue", "worker"] },
{ "kind": "security-group", "label": "sg-api :443/:8000", "wraps": ["lb", "api"] }
],
"connections": [
{ "from": "users", "to": "cdn", "label": "HTTPS", "variant": "emphasis" },
{ "from": "auth", "to": "api", "label": "verify JWT", "variant": "security", "fromSide": "right", "toSide": "left", "via": [[620, 142], [620, 330]] },
{ "from": "cdn", "to": "lb" },
{ "from": "cdn", "to": "s3", "label": "static", "variant": "dashed", "fromSide": "bottom", "toSide": "top", "labelDy": 58 },
{ "from": "lb", "to": "api" },
{ "from": "api", "to": "cache", "label": "read-through", "fromSide": "top", "toSide": "bottom", "labelDy": -68 },
{ "from": "api", "to": "db", "label": "SQL" },
{ "from": "api", "to": "queue", "label": "enqueue", "variant": "dashed", "fromSide": "bottom", "toSide": "top", "labelDy": 58 },
{ "from": "queue", "to": "worker" }
],
"cards": [
{ "dot": "cyan", "title": "Edge", "items": ["CloudFront CDN fronts all traffic", "S3 serves static assets via OAI"] },
{ "dot": "emerald", "title": "Application", "items": ["FastAPI behind an HTTPS load balancer", "Redis read-through cache", "Async work drained from SQS by a worker"] },
{ "dot": "rose", "title": "Security", "items": ["OAuth 2.0 with JWT + PKCE", "API + LB isolated in a security group"] }
]
}
@@ -0,0 +1,22 @@
{
"schema_version": 1,
"diagram_type": "workflow",
"meta": {
"title": "Legacy narrow workflow",
"viewBox": [700, 400]
},
"lanes": [
{ "id": "first", "label": "First" },
{ "id": "second", "label": "Second" }
],
"nodes": [
{ "id": "node_0", "lane": "first", "col": 0, "type": "frontend", "label": "frontend" },
{ "id": "node_1", "lane": "first", "col": 2, "type": "backend", "label": "backend" },
{ "id": "node_2", "lane": "first", "col": 4, "type": "database", "label": "database" },
{ "id": "node_3", "lane": "second", "col": 0, "type": "cloud", "label": "cloud" },
{ "id": "node_4", "lane": "second", "col": 2, "type": "security", "label": "security" },
{ "id": "node_5", "lane": "second", "col": 4, "type": "messagebus", "label": "messagebus" },
{ "id": "node_6", "lane": "second", "col": 5, "type": "external", "label": "external" }
],
"edges": []
}
@@ -0,0 +1,47 @@
{
"schema_version": 1,
"diagram_type": "workflow",
"meta": {
"title": "Pinned coordinate migration",
"viewBox": [720, 700],
"legend": { "mode": "hidden" }
},
"lanes": [
{ "id": "route", "label": "Explicit route" },
{ "id": "label", "label": "Explicit label" },
{ "id": "channel-source", "label": "Channel source" },
{ "id": "channel-target", "label": "Channel target" }
],
"nodes": [
{ "id": "route-a", "lane": "route", "col": 0, "type": "backend", "label": "A" },
{ "id": "route-b", "lane": "route", "col": 2, "type": "backend", "label": "B" },
{ "id": "label-a", "lane": "label", "col": 2, "type": "backend", "label": "C" },
{ "id": "label-b", "lane": "label", "col": 3, "type": "backend", "label": "D" },
{ "id": "channel-a", "lane": "channel-source", "col": 1, "type": "backend", "label": "E" },
{ "id": "channel-b", "lane": "channel-target", "col": 1, "type": "backend", "label": "F" }
],
"edges": [
{
"id": "pinned-via",
"from": "route-a",
"to": "route-b",
"via": [[220, 119]]
},
{
"id": "pinned-label",
"from": "label-a",
"to": "label-b",
"label": "ok",
"labelAt": [365, 203]
},
{
"id": "pinned-channel",
"from": "channel-a",
"to": "channel-b",
"fromSide": "right",
"toSide": "right",
"route": "outside-right",
"channelX": 500
}
]
}
@@ -0,0 +1,130 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { SCENARIO_RECIPES } from '../recipes/scenarios.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(skillRoot, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-gallery-'));
const generatedRoot = path.join(tmp, 'docs');
function sha256(file) {
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
}
function normalize(text) {
return text.replace(/\r\n?/g, '\n');
}
test('generated proof gallery matches its sources, receipts, and checked-in artifacts', () => {
const output = execFileSync(process.execPath, [
path.join(repoRoot, 'scripts', 'build-gallery.mjs'),
generatedRoot,
], { encoding: 'utf8' });
assert.match(output, /gallery 11 artifacts \/ 99 checks/);
const manifestPath = path.join(generatedRoot, 'gallery', 'manifest.json');
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
assert.equal(manifest.schemaVersion, 1);
assert.equal(manifest.archifyVersion, JSON.parse(fs.readFileSync(path.join(skillRoot, 'package.json'))).version);
assert.equal(manifest.entryCount, 11);
assert.equal(manifest.checkCount, 99);
assert.deepEqual(new Set(manifest.entries.map((entry) => entry.type)), new Set([
'architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle',
]));
assert.deepEqual(
Object.fromEntries(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'].map((type) => [
type,
manifest.entries.filter((entry) => entry.type === type).length,
])),
{ architecture: 2, workflow: 3, sequence: 2, dataflow: 2, lifecycle: 2 },
);
assert.deepEqual(
new Set(manifest.entries.map((entry) => entry.id)),
new Set(SCENARIO_RECIPES.map((recipe) => recipe.proof)),
);
const workflow = manifest.entries.find((entry) => entry.id === 'agent-tool-call');
assert.equal(workflow.view, 'happy-path');
assert.equal(workflow.viewCount, 3);
assert.deepEqual(workflow.viewIds, ['happy-path', 'safety-gate', 'evidence-loop']);
assert.equal(workflow.guidedPlayback, true);
const deployment = manifest.entries.find((entry) => entry.id === 'deployment-ownership');
assert.equal(deployment.engineeringProfile, 'deployment-ownership');
assert.ok(manifest.entries.filter((entry) => entry.id !== 'deployment-ownership')
.every((entry) => entry.engineeringProfile === null));
for (const entry of manifest.entries) {
const artifact = path.join(generatedRoot, entry.artifact.replace(/^gallery\//, 'gallery/'));
const source = path.join(generatedRoot, entry.input.replace(/^gallery\//, 'gallery/'));
assert.ok(fs.existsSync(artifact), `${entry.id}: artifact missing`);
assert.ok(fs.existsSync(source), `${entry.id}: source missing`);
assert.equal(sha256(artifact), entry.artifactSha256, `${entry.id}: artifact digest drift`);
assert.equal(sha256(source), entry.sourceSha256, `${entry.id}: source digest drift`);
assert.equal(entry.checks.length, 9);
assert.ok(entry.checks.every((check) => check.ok), `${entry.id}: validation receipt not green`);
assert.equal(entry.composition.profile, 'showcase', `${entry.id}: expected showcase composition profile`);
assert.equal(entry.composition.status, 'pass', `${entry.id}: showcase composition is not green`);
assert.equal(entry.composition.metrics.properCrossings, 0, `${entry.id}: proper crossing debt remains`);
assert.equal(entry.composition.metrics.ambiguousCorridors, 0, `${entry.id}: ambiguous corridor debt remains`);
assert.equal(entry.composition.metrics.containerBorderRuns, 0, `${entry.id}: container border-run debt remains`);
assert.equal(entry.composition.metrics.labelRouteClearanceIssues, 0, `${entry.id}: label-route clearance debt remains`);
assert.equal(entry.composition.metrics.shortInteriorSegmentCount, 0, `${entry.id}: cramped interior turn remains`);
assert.equal(entry.composition.metrics.microSegmentCount, 0, `${entry.id}: micro segment remains`);
assert.equal(entry.viewCount, 3, `${entry.id}: expected a three-step reader story`);
assert.equal(entry.guidedPlayback, true, `${entry.id}: guided playback missing`);
}
const html = fs.readFileSync(path.join(generatedRoot, 'gallery.html'), 'utf8');
assert.equal((html.match(/class="showcase-card/g) || []).length, 11);
assert.match(html, /id="gallery-manifest" type="application\/json"/);
assert.match(html, /data-src-base="gallery\/artifacts\/agent-tool-call\.workflow\.html"/);
assert.match(html, /agent-tool-call\.workflow\.html\?present=1&amp;play=1#view=happy-path/);
assert.match(html, /event-stream\.dataflow\.html\?present=1&amp;play=1#view=order-transit/);
assert.match(html, /id="proof-deployment-lifecycle"/);
assert.match(html, /Play named chapter/);
assert.match(html, /3 views · play/);
assert.match(html, /Proof,<br><em>not promises\.<\/em>/);
assert.match(html, /Five lenses\. Eleven real stories\./);
assert.match(html, /Composition<\/span><span class="receipt-value ok" title="0 crossings · 0 border runs · 0 micro segments · 0 cramped turns">SHOWCASE · PASS/);
assert.match(html, /Engineering profile/);
assert.match(html, /DEPLOYMENT OWNERSHIP · PASS/);
assert.match(html, /<link rel="stylesheet" href="assets\/site-navigation\.css">/);
assert.match(
fs.readFileSync(path.join(generatedRoot, 'assets/site-navigation.css'), 'utf8'),
/\.site-nav \.nav-logo \{[^}]*min-height: 44px;/,
);
assert.match(html, /\.filter-button \{\s+min-height: 44px;/);
assert.match(html, /\.card-link \{ min-height: 44px;/);
assert.equal((html.match(/class="card-link create-link"/g) || []).length, 11);
for (const type of ['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']) {
assert.match(html, new RegExp(`start\\.html\\?type=${type}&amp;source=gallery`), `${type}: gallery-to-start link missing`);
}
assert.match(html, /class="community-callout"/);
assert.match(html, /href="https:\/\/github\.com\/tt-a1i\/archify\/issues\/new\?template=showcase\.yml"[^>]+rel="noopener noreferrer"/);
assert.match(html, /Share a verified diagram/);
assert.match(html, /提交已验证成品/);
for (const relative of [
'gallery.html',
'assets/site-language.js',
'assets/site-navigation.css',
'gallery/manifest.json',
...manifest.entries.flatMap((entry) => [entry.artifact, entry.input]),
]) {
const fresh = path.join(generatedRoot, relative);
const checked = path.join(repoRoot, 'docs', relative);
assert.ok(fs.existsSync(checked), `${relative}: checked-in gallery output missing`);
assert.equal(normalize(fs.readFileSync(fresh, 'utf8')), normalize(fs.readFileSync(checked, 'utf8')),
`${relative}: checked-in gallery output is stale; run node scripts/build-gallery.mjs`);
}
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,59 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { workflow as validateWorkflow } from '../renderers/shared/generated-validators.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
function workflowDocument(schemaVersion) {
return {
schema_version: schemaVersion,
diagram_type: 'workflow',
meta: { title: 'Schema compatibility' },
lanes: [{ id: 'main', label: 'Main' }],
nodes: [{ id: 'step', lane: 'main', col: 0, type: 'backend', label: 'Step' }],
edges: [],
};
}
test('generated workflow validator accepts schema versions 1 and 2 only', () => {
assert.equal(validateWorkflow(workflowDocument(1)), true, JSON.stringify(validateWorkflow.errors));
assert.equal(validateWorkflow(workflowDocument(2)), true, JSON.stringify(validateWorkflow.errors));
assert.equal(validateWorkflow(workflowDocument(3)), false);
assert.deepEqual(validateWorkflow.errors?.[0]?.params.allowedValues, [1, 2]);
});
test('validator freshness check accepts CRLF checkouts', () => {
const scratch = fs.mkdtempSync(path.join(skillRoot, '.validator-check-'));
try {
fs.mkdirSync(path.join(scratch, 'scripts'));
fs.mkdirSync(path.join(scratch, 'renderers', 'shared'), { recursive: true });
fs.cpSync(path.join(skillRoot, 'schemas'), path.join(scratch, 'schemas'), { recursive: true });
fs.copyFileSync(
path.join(skillRoot, 'scripts', 'generate-validators.mjs'),
path.join(scratch, 'scripts', 'generate-validators.mjs'),
);
const validator = fs.readFileSync(
path.join(skillRoot, 'renderers', 'shared', 'generated-validators.mjs'),
'utf8',
);
fs.writeFileSync(
path.join(scratch, 'renderers', 'shared', 'generated-validators.mjs'),
validator.replace(/\r\n?|\n/g, '\r\n'),
);
const result = spawnSync(process.execPath, [
path.join(scratch, 'scripts', 'generate-validators.mjs'),
'--check',
], { encoding: 'utf8' });
assert.equal(result.status, 0, result.stderr);
} finally {
fs.rmSync(scratch, { recursive: true, force: true });
}
});
@@ -0,0 +1,88 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { extractSvgs, parseXml } from './helpers/xml.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(skillRoot, '..');
const artifactRoots = [
'archify/examples',
'docs',
'examples',
'experiments',
];
function trackedHtmlArtifacts() {
const tracked = spawnSync('git', ['ls-files', '-z', '--', ...artifactRoots], {
cwd: repoRoot,
encoding: 'buffer',
});
assert.equal(tracked.status, 0, tracked.stderr.toString());
return tracked.stdout.toString()
.split('\0')
.filter((entry) => entry.endsWith('.html'))
.sort();
}
test('artifact SVG extraction follows HTML quoting and preserves SVG document boundaries', () => {
const extracted = extractSvgs(`
<script>const ignored = '<svg data-node-label></svg>';</script>
<template><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"/></template>
<iframe srcdoc='&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;&lt;svg viewBox=&quot;0 0 1 1&quot;/&gt;&lt;/svg&gt;'></iframe>
`);
assert.equal(extracted.direct.length, 1, 'template SVG is markup while script text is not');
assert.equal(extracted.embedded.length, 1, 'srcdoc contributes one top-level SVG document');
for (const svg of [...extracted.direct, ...extracted.embedded]) assert.doesNotThrow(() => parseXml(svg));
const inheritedNamespace = extractSvgs(`
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<svg><use xlink:href="#icon"/></svg>
</svg>
`);
assert.equal(inheritedNamespace.direct.length, 1, 'nested SVG remains inside its XML document');
assert.doesNotThrow(() => parseXml(inheritedNamespace.direct[0]));
});
test('tracked browsable HTML embeds well-formed XML SVG', () => {
const artifacts = trackedHtmlArtifacts();
const checkoutArtifact = 'examples/checkout-platform-delta.html';
assert.ok(artifacts.includes(checkoutArtifact), 'expected the tracked Checkout compare artifact');
let checkoutSvgs;
for (const relative of artifacts) {
const html = fs.readFileSync(path.join(repoRoot, relative), 'utf8');
const extracted = extractSvgs(html);
if (relative === checkoutArtifact) checkoutSvgs = extracted;
const svgs = [...extracted.direct, ...extracted.embedded];
if (svgs.length === 0) continue;
for (const [index, svg] of svgs.entries()) {
assert.doesNotThrow(
() => parseXml(svg),
`${relative}: SVG ${index + 1} must be well-formed XML`,
);
}
}
assert.equal(checkoutSvgs?.direct.length, 1, 'Checkout must contain one comparison SVG');
assert.equal(checkoutSvgs?.embedded.length, 2, 'Checkout must retain its base/head SVG snapshots');
});
test('legacy example URLs redirect to the current canonical artifacts', () => {
for (const [legacy, canonical] of [
['examples/workflow-agent-tool-call.html', 'workflow-agent-tool-call-rendered.html'],
['examples/sequence-cache-miss.html', 'sequence-cache-miss-request.html'],
]) {
const html = fs.readFileSync(path.join(repoRoot, legacy), 'utf8');
assert.match(html, new RegExp(`<link rel="canonical" href="${canonical}">`));
assert.match(
html,
new RegExp(`window\\.location\\.replace\\("${canonical}" \\+ window\\.location\\.search \\+ window\\.location\\.hash\\)`),
`${legacy} must preserve query parameters and deep-link fragments`,
);
assert.equal(fs.existsSync(path.join(repoRoot, 'examples', canonical)), true);
}
});
@@ -0,0 +1,739 @@
// Unit tests for the pure geometry/text helpers that every renderer leans on.
// These are exercised only transitively by the golden byte-compares, which
// can't distinguish a geometry regression from an intentional layout change —
// so they get a direct oracle here. Zero deps: node:test + node:assert.
//
// node --test test/*.test.mjs (or: npm test)
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
rectsOverlap,
segmentIntersectsRect,
segmentRectClearance,
segmentRectIntersectionLength,
collectLabelRouteClearance,
cleanEndpointSideProblems,
cleanFlowProblems,
cleanCrossingProblems,
collectAmbiguousCorridors,
cleanAmbiguousCorridorProblems,
collectBorderRuns,
cleanBorderRunProblems,
collectRouteRhythmIssues,
cleanRouteRhythmProblems,
routeBudgetMetrics,
asArray,
isFinitePoint,
anchor,
automaticPortRhythmBridge,
defaultFromSide,
defaultToSide,
chosenSide,
routeHonorsEndpointSides,
polylinePath,
roundedPath,
labelPoint,
suggestLabelObstacleFix,
suggestComponentSeparation,
} from '../renderers/shared/geometry.mjs';
import { textUnits, applyTemplate, renderSemanticSigil } from '../renderers/shared/utils.mjs';
const rect = (x, y, w, h) => ({ x, y, width: w, height: h, cx: x + w / 2, cy: y + h / 2 });
test('automaticPortRhythmBridge: near parallel ports use readable outside runs', () => {
const points = automaticPortRhythmBridge(
[742, 300],
[735, 180],
'top',
'bottom',
);
assert.deepEqual(points, [
[742, 300],
[742, 276],
[758, 276],
[758, 204],
[735, 204],
[735, 180],
]);
assert.deepEqual(collectRouteRhythmIssues({
routedRelations: [{ relation: { id: 'read' }, points }],
}), []);
});
test('rectsOverlap: separated rects do not overlap', () => {
assert.equal(rectsOverlap(rect(0, 0, 10, 10), rect(20, 0, 10, 10)), false);
});
test('rectsOverlap: clearly overlapping rects overlap', () => {
assert.equal(rectsOverlap(rect(0, 0, 10, 10), rect(5, 5, 10, 10)), true);
});
test('rectsOverlap: edge-touching is NOT overlap at gap 0 (<= boundary)', () => {
// a ends at x=10, b starts at x=10 — exactly touching.
assert.equal(rectsOverlap(rect(0, 0, 10, 10), rect(10, 0, 10, 10), 0), false);
});
test('rectsOverlap: positive gap flags rects within that gap as too close', () => {
// 8px apart, required gap 8 → touching the threshold counts as too close.
assert.equal(rectsOverlap(rect(0, 0, 10, 10), rect(18, 0, 10, 10), 8), false);
assert.equal(rectsOverlap(rect(0, 0, 10, 10), rect(17, 0, 10, 10), 8), true);
});
test('rectsOverlap: negative gap shrinks the hit box (label-collision convention)', () => {
// gap -2 means rects must overlap by MORE than 2px to count — a 1px sliver
// does not. This is the sign convention the label checks rely on.
assert.equal(rectsOverlap(rect(0, 0, 10, 10), rect(9, 0, 10, 10), -2), false);
assert.equal(rectsOverlap(rect(0, 0, 10, 10), rect(7, 0, 10, 10), -2), true);
});
test('rectsOverlap: non-finite geometry is not an overlap', () => {
// A component authored without pos lands here as NaN. Every comparison in the
// negated form is false for NaN, so the unguarded version reported a collision
// for every pair and buried the real "must include pos" diagnostic.
const nan = rect(Number.NaN, Number.NaN, 120, 60);
assert.equal(rectsOverlap(nan, nan, 8), false);
assert.equal(rectsOverlap(nan, rect(0, 0, 10, 10), 8), false);
assert.equal(rectsOverlap(rect(0, 0, 10, 10), nan, 8), false);
assert.equal(rectsOverlap(rect(0, 0, 10, 10), rect(20, 0, Number.NaN, 10)), false);
assert.equal(rectsOverlap(rect(0, 0, 10, 10), rect(5, 5, 10, Number.POSITIVE_INFINITY)), false);
});
test('segmentIntersectsRect: detects an edge crossing a node box', () => {
assert.equal(segmentIntersectsRect({ start: [0, 5], end: [20, 5] }, rect(8, 0, 4, 10)), true);
assert.equal(segmentIntersectsRect({ start: [0, 20], end: [20, 20] }, rect(8, 0, 4, 10)), false);
});
test('segmentRectClearance measures horizontal, vertical, and reversed diagonal segments', () => {
const box = rect(10, 10, 10, 10);
assert.equal(segmentRectClearance({ start: [0, 6], end: [30, 6] }, box), 4);
assert.equal(segmentRectClearance({ start: [6, 0], end: [6, 30] }, box), 4);
assert.equal(segmentRectClearance({ start: [0, 0], end: [8, 8] }, box), Math.sqrt(8));
assert.equal(segmentRectClearance({ start: [8, 8], end: [0, 0] }, box), Math.sqrt(8));
assert.equal(segmentRectClearance({ start: [0, 15], end: [30, 15] }, box), 0);
});
test('label-route clearance locks tangent, sub-threshold, boundary, and reversed coordinates', () => {
const box = rect(10, 10, 10, 10);
const cases = [
{ segment: { start: [0, 10], end: [30, 10] }, clearance: 0, intersection: 10 },
{ segment: { start: [10, 0], end: [10, 30] }, clearance: 0, intersection: 10 },
{ segment: { start: [0, 0], end: [30, 30] }, clearance: 0, intersection: Math.sqrt(200) },
{ segment: { start: [0, 0], end: [10, 10] }, clearance: 0, intersection: 0 },
{ segment: { start: [0, 8.1], end: [30, 8.1] }, clearance: 1.9, intersection: 0 },
{ segment: { start: [0, 8], end: [30, 8] }, clearance: 2, intersection: 0 },
{ segment: { start: [0, 6.1], end: [30, 6.1] }, clearance: 3.9, intersection: 0 },
{ segment: { start: [0, 6], end: [30, 6] }, clearance: 4, intersection: 0 },
{ segment: { start: [0, 0], end: [5, 0] }, clearance: Math.sqrt(125), intersection: 0 },
];
for (const { segment, clearance, intersection } of cases) {
assert.ok(Math.abs(segmentRectClearance(segment, box) - clearance) < 0.000001);
assert.ok(Math.abs(segmentRectIntersectionLength(segment, box) - intersection) < 0.000001);
const reversed = { start: segment.end, end: segment.start };
assert.ok(Math.abs(segmentRectClearance(reversed, box) - clearance) < 0.000001);
assert.ok(Math.abs(segmentRectIntersectionLength(reversed, box) - intersection) < 0.000001);
}
});
test('collectLabelRouteClearance exempts only the owning relationship at an exact threshold', () => {
const owner = { id: 'owner', from: 'a', to: 'b' };
const sharedSource = { id: 'other', from: 'a', to: 'c' };
const labels = [{ relation: owner, relationIndex: 0, label: 'handoff', ...rect(80, 48, 60, 14) }];
const routedRelations = [
{ relation: owner, relationIndex: 0, points: [[20, 60], [200, 60]] },
{ relation: sharedSource, relationIndex: 1, points: [[70, 64], [150, 64]] },
];
assert.deepEqual(collectLabelRouteClearance({ labels, routedRelations, threshold: 2 }), []);
const hits = collectLabelRouteClearance({ labels, routedRelations, threshold: 4 });
assert.equal(hits.length, 1);
assert.equal(hits[0].clearance, 2);
assert.equal(hits[0].otherRelation, sharedSource);
});
test('endpoint-side direction distinguishes perpendicular entry from a tangent border run', () => {
const clean = [[350, 160], [350, 200], [150, 200], [150, 240]];
const tangent = [[350, 160], [350, 200], [100, 200], [100, 240], [150, 240]];
assert.equal(routeHonorsEndpointSides(clean, 'bottom', 'top'), true);
assert.equal(routeHonorsEndpointSides(tangent, 'bottom', 'top'), false);
const relation = { id: 'tasks-file', from: 'cli-agents', to: 'tasks-watch', fromSide: 'bottom', toSide: 'top' };
const problems = cleanEndpointSideProblems({
relations: [relation],
endpointIds: new Set(['cli-agents', 'tasks-watch']),
pathFor: () => ({ points: tangent }),
diagramType: 'architecture',
relationCollection: 'connections',
});
assert.equal(problems.length, 1);
assert.match(problems[0], /\[clean-flow\/endpoint-side-direction\] architecture connections\[0\] id "tasks-file"/);
assert.match(problems[0], /final segment 3 \[100, 240\] -> \[150, 240\]/);
assert.match(problems[0], /toSide "top".*vertical downward from above/);
});
test('endpoint-side direction can fail closed on renderer-inferred automatic sides', () => {
const relation = { id: 'terminal-return', from: 'stream-hub', to: 'workspace' };
const problems = cleanEndpointSideProblems({
relations: [relation],
endpointIds: new Set(['stream-hub', 'workspace']),
pathFor: () => ({ points: [[700, 130], [700, 230], [160, 230], [160, 330]] }),
diagramType: 'architecture',
relationCollection: 'connections',
fromSideFor: () => 'left',
toSideFor: () => 'right',
});
assert.equal(problems.length, 2);
assert.match(problems[0], /inferred fromSide "left"/);
assert.match(problems[1], /inferred toSide "right"/);
});
test('cleanFlowProblems reports collection index, ids, segment, clearance, and fix', () => {
const relations = [{ id: 'checkout', from: 'client', to: 'database' }];
const obstacles = [
{ id: 'client', ...rect(0, 0, 20, 20) },
{ id: 'proxy', ...rect(40, 0, 20, 20) },
{ id: 'database', ...rect(80, 0, 20, 20) },
];
const problems = cleanFlowProblems({
relations,
obstacles,
pathFor: () => ({ points: [[20, 10], [80, 10]] }),
diagramType: 'architecture',
relationCollection: 'connections',
obstacleKind: 'component',
routeHint: 'set route/via'
});
assert.equal(problems.length, 1);
assert.match(problems[0], /\[clean-flow\/edge-through-node\] architecture connections\[0\] id "checkout" "client" -> "database"/);
assert.match(problems[0], /crosses component "proxy"/);
assert.match(problems[0], /segment 0 \[20, 10\] -> \[80, 10\] \(2px clearance\)/);
assert.match(problems[0], /set route\/via/);
});
test('cleanFlowProblems exempts endpoints and ignores missing endpoint geometry', () => {
const endpointOnly = cleanFlowProblems({
relations: [{ from: 'a', to: 'b' }],
obstacles: [{ id: 'a', ...rect(0, 0, 20, 20) }, { id: 'b', ...rect(80, 0, 20, 20) }],
pathFor: () => ({ points: [[20, 10], [80, 10]] }),
diagramType: 'workflow',
relationCollection: 'edges',
obstacleKind: 'node'
});
assert.deepEqual(endpointOnly, []);
let pathCalled = false;
const missingEndpoint = cleanFlowProblems({
relations: [{ from: 'a', to: 'ghost' }],
obstacles: [{ id: 'a', ...rect(0, 0, 20, 20) }],
pathFor: () => { pathCalled = true; return { points: [] }; },
diagramType: 'workflow',
relationCollection: 'edges',
obstacleKind: 'node'
});
assert.deepEqual(missingEndpoint, []);
assert.equal(pathCalled, false);
});
test('cleanFlowProblems uses clearance, reports the first segment, and deduplicates an obstacle', () => {
const problems = cleanFlowProblems({
relations: [{ from: 'a', to: 'b' }],
obstacles: [
{ id: 'a', ...rect(-20, -10, 20, 20) },
{ id: 'near', ...rect(8, 1, 4, 2) },
{ id: 'b', ...rect(20, -10, 20, 20) },
],
// Both segment 0 (within the 2px halo) and segment 2 intersect `near`.
pathFor: () => ({ points: [[0, -1], [20, -1], [0, 5], [20, 5]] }),
diagramType: 'workflow',
relationCollection: 'edges',
obstacleKind: 'node'
});
assert.equal(problems.length, 1);
assert.match(problems[0], /segment 0 \[0, -1\] -> \[20, -1\]/);
});
test('cleanCrossingProblems reports one deterministic proper X in showcase', () => {
const first = { id: 'first', from: 'a', to: 'b' };
const second = { id: 'second', from: 'c', to: 'd' };
const routes = new Map([
[first, { points: [[0, 0], [100, 0], [100, 100]] }],
[second, { points: [[50, -50], [50, 50], [150, 50], [150, -50], [50, -50]] }],
]);
const problems = cleanCrossingProblems({
relations: [first, second],
endpointIds: new Set(['a', 'b', 'c', 'd']),
pathFor: (relation) => routes.get(relation),
diagramType: 'architecture',
relationCollection: 'connections',
profile: 'showcase',
routeHint: 'move a via point',
});
assert.equal(problems.length, 1);
assert.match(problems[0], /\[composition\/proper-crossing\] showcase architecture/);
assert.match(problems[0], /connections\[0\] id "first" "a" -> "b" crosses connections\[1\] id "second" "c" -> "d"/);
assert.match(problems[0], /at \[50, 0\] \(segments 0 and 0\)/);
assert.match(problems[0], /move a via point/);
});
test('cleanCrossingProblems keeps proper X as non-blocking in standard', () => {
const relations = [{ from: 'a', to: 'b' }, { from: 'c', to: 'd' }];
const routes = [[[0, 50], [100, 50]], [[50, 0], [50, 100]]];
const problems = cleanCrossingProblems({
relations,
endpointIds: new Set(['a', 'b', 'c', 'd']),
pathFor: (relation) => ({ points: routes[relations.indexOf(relation)] }),
diagramType: 'workflow',
relationCollection: 'edges',
profile: 'standard',
});
assert.deepEqual(problems, []);
});
test('cleanCrossingProblems exempts shared endpoints', () => {
const relations = [{ from: 'a', to: 'b' }, { from: 'a', to: 'c' }];
const routes = [[[0, 50], [100, 50]], [[50, 0], [50, 100]]];
const problems = cleanCrossingProblems({
relations,
endpointIds: new Set(['a', 'b', 'c']),
pathFor: (relation) => ({ points: routes[relations.indexOf(relation)] }),
diagramType: 'dataflow',
relationCollection: 'flows',
profile: 'showcase',
});
assert.deepEqual(problems, []);
});
test('cleanCrossingProblems exempts endpoint touches and collinear corridors', () => {
const relations = [
{ from: 'a', to: 'b' },
{ from: 'c', to: 'd' },
{ from: 'e', to: 'f' },
];
const routes = [
[[0, 0], [100, 0]],
[[50, 0], [50, 50]],
[[25, 0], [75, 0]],
];
const problems = cleanCrossingProblems({
relations,
endpointIds: new Set(['a', 'b', 'c', 'd', 'e', 'f']),
pathFor: (relation) => ({ points: routes[relations.indexOf(relation)] }),
diagramType: 'lifecycle',
relationCollection: 'transitions',
profile: 'showcase',
});
assert.deepEqual(problems, []);
});
test('ambiguous corridor gate reports unrelated collinear overlap with exact identities', () => {
const first = { id: 'first', from: 'a', to: 'b' };
const second = { id: 'second', from: 'c', to: 'd' };
const routes = new Map([
[first, { points: [[0, 20], [100, 20], [100, 80]] }],
[second, { points: [[40, 20], [140, 20], [140, 80]] }],
]);
const hits = collectAmbiguousCorridors({
routedRelations: [first, second].map((relation, relationIndex) => ({
relation,
relationIndex,
points: routes.get(relation).points,
})),
});
assert.equal(hits.length, 1);
assert.equal(hits[0].overlapLength, 60);
assert.deepEqual(hits[0].overlapStart, [40, 20]);
assert.deepEqual(hits[0].overlapEnd, [100, 20]);
const problems = cleanAmbiguousCorridorProblems({
relations: [first, second],
endpointIds: new Set(['a', 'b', 'c', 'd']),
pathFor: (relation) => routes.get(relation),
diagramType: 'workflow',
relationCollection: 'edges',
profile: 'showcase',
routeHint: 'move a channel',
});
assert.equal(problems.length, 1);
assert.match(problems[0], /\[composition\/ambiguous-corridor\] showcase workflow/);
assert.match(problems[0], /edges\[0\] id "first" "a" -> "b" shares a 60px corridor with edges\[1\] id "second" "c" -> "d"/);
assert.match(problems[0], /\[40, 20\] -> \[100, 20\].*move a channel/);
});
test('ambiguous corridor gate exempts shared endpoints, point touches, and overlaps below 8px', () => {
const routedRelations = [
{ relation: { from: 'a', to: 'b' }, relationIndex: 0, points: [[0, 20], [100, 20]] },
{ relation: { from: 'a', to: 'c' }, relationIndex: 1, points: [[40, 20], [140, 20]] },
{ relation: { from: 'd', to: 'e' }, relationIndex: 2, points: [[100, 20], [100, 80]] },
{ relation: { from: 'f', to: 'g' }, relationIndex: 3, points: [[94, 60], [101, 60]] },
{ relation: { from: 'h', to: 'i' }, relationIndex: 4, points: [[98, 60], [110, 60]] },
];
assert.deepEqual(collectAmbiguousCorridors({ routedRelations }), []);
});
test('ambiguous corridor gate keeps standard renderable', () => {
const relations = [{ from: 'a', to: 'b' }, { from: 'c', to: 'd' }];
const routes = [[[0, 20], [100, 20]], [[40, 20], [140, 20]]];
assert.deepEqual(cleanAmbiguousCorridorProblems({
relations,
endpointIds: new Set(['a', 'b', 'c', 'd']),
pathFor: (relation) => ({ points: routes[relations.indexOf(relation)] }),
diagramType: 'architecture',
relationCollection: 'connections',
profile: 'standard',
}), []);
});
test('cleanBorderRunProblems reports a deterministic long run on a rounded frame side', () => {
const relation = { id: 'jwt', from: 'auth', to: 'api' };
const problems = cleanBorderRunProblems({
relations: [relation],
frames: [{ id: 'private', label: 'Private tier', kind: 'security-group', x: 100, y: 80, width: 180, height: 120, radius: 8 }],
pathFor: () => ({ points: [[40, 80], [220, 80], [220, 140]] }),
diagramType: 'architecture',
relationCollection: 'connections',
profile: 'standard',
routeHint: 'move the via point',
});
assert.equal(problems.length, 1);
assert.match(problems[0], /\[composition\/container-border-run\] architecture connections\[0\] id "jwt" "auth" -> "api"/);
assert.match(problems[0], /follows security-group "Private tier" top border for 112px on segment 0 \[108, 80\] -> \[220, 80\]/);
assert.match(problems[0], /move the via point/);
});
test('border-run contract allows perpendicular crossings, point touches, and rounded corners', () => {
const frame = { id: 'stage', kind: 'stage', x: 40, y: 40, width: 120, height: 100, radius: 10 };
const routedRelations = [
{ relation: { from: 'a', to: 'b' }, relationIndex: 0, points: [[100, 10], [100, 80]] },
{ relation: { from: 'c', to: 'd' }, relationIndex: 1, points: [[20, 40], [40, 40], [40, 20]] },
{ relation: { from: 'e', to: 'f' }, relationIndex: 2, points: [[40, 40], [49, 40]] },
];
assert.deepEqual(collectBorderRuns({ routedRelations, frames: [frame] }), []);
});
test('border-run contract detects vertical frames and merges hits per relation side', () => {
const hits = collectBorderRuns({
routedRelations: [{
relation: { from: 'a', to: 'b' },
relationIndex: 3,
points: [[160, 60], [160, 110], [150, 110], [160, 110], [160, 135]],
}],
frames: [{ kind: 'lane', id: 'lane-1', x: 40, y: 40, width: 120, height: 100, radius: 10 }],
});
assert.equal(hits.length, 1);
assert.equal(hits[0].side, 'right');
assert.equal(hits[0].segmentIndex, 0);
assert.equal(hits[0].overlapLength, 70);
});
test('border-run contract merges adjacent primitives and counts any positive straight overlap', () => {
const hits = collectBorderRuns({
routedRelations: [{
relation: { from: 'a', to: 'b' },
relationIndex: 0,
points: [[52, 40], [70, 40], [90, 40], [90, 50]],
}],
frames: [{ kind: 'stage', id: 'source', x: 40, y: 40, width: 120, height: 100, radius: 10 }],
});
assert.equal(hits.length, 1);
assert.equal(hits[0].overlapLength, 38);
assert.deepEqual(hits[0].overlapStart, [52, 40]);
assert.deepEqual(hits[0].overlapEnd, [90, 40]);
});
test('routeBudgetMetrics normalizes collinear points and records neutral route evidence', () => {
const metrics = routeBudgetMetrics({
routedRelations: [
{ points: [[0, 0], [10, 0], [30, 0], [30, 8], [50, 8], [50, 30]] },
{ points: [[5, 5], [5, 5]] },
],
});
assert.deepEqual(metrics, {
maxBends: 3,
routesOverSuggestedBends: 1,
maxStretch: 80 / 80,
routesOverSuggestedStretch: 0,
minSegmentPx: 8,
minInteriorSegmentPx: 8,
shortSegmentCount: 1,
shortEndpointSegmentCount: 0,
shortInteriorSegmentCount: 1,
microSegmentCount: 0,
});
});
test('route rhythm separates ordinary endpoint stubs from cramped turns and micro segments', () => {
const issues = collectRouteRhythmIssues({
routedRelations: [
{ relation: { id: 'lane-hop', from: 'a', to: 'b' }, points: [[0, 0], [13, 0], [13, 40], [80, 40], [80, 53]] },
{ relation: { id: 'bad-turn', from: 'c', to: 'd' }, points: [[0, 80], [24, 80], [24, 89], [60, 89]] },
{ relation: { id: 'micro-stub', from: 'e', to: 'f' }, points: [[0, 120], [5, 120], [5, 180]] },
],
});
assert.deepEqual(issues.map((issue) => [issue.relation.id, issue.code, issue.position, issue.length]), [
['bad-turn', 'composition/short-interior-segment', 'interior', 9],
['micro-stub', 'composition/micro-segment', 'source-stub', 5],
]);
});
test('route rhythm is a showcase-only generation gate with actionable relationship identity', () => {
const relations = [{ id: 'events', from: 'api', to: 'bus' }];
const args = {
relations,
endpointIds: new Set(['api', 'bus']),
pathFor: () => ({ points: [[10, 20], [15, 20], [15, 80]] }),
diagramType: 'architecture',
relationCollection: 'connections',
};
assert.deepEqual(cleanRouteRhythmProblems({ ...args, profile: 'standard' }), []);
const problems = cleanRouteRhythmProblems({ ...args, profile: 'showcase' });
assert.equal(problems.length, 1);
assert.match(problems[0], /\[composition\/micro-segment\] showcase architecture connections\[0\] id "events"/);
assert.match(problems[0], /5px source-stub segment 0/);
});
test('asArray coerces non-arrays to [] (degraded-mode guard)', () => {
assert.deepEqual(asArray([1, 2]), [1, 2]);
assert.deepEqual(asArray('oops'), []);
assert.deepEqual(asArray(undefined), []);
assert.deepEqual(asArray(null), []);
assert.deepEqual(asArray({ length: 3 }), []);
});
test('isFinitePoint rejects NaN/undefined/Infinity', () => {
assert.equal(isFinitePoint(1, 2, 3, 4), true);
assert.equal(isFinitePoint(1, NaN), false);
assert.equal(isFinitePoint(1, undefined), false);
assert.equal(isFinitePoint(1, Infinity), false);
});
test('anchor returns the correct edge midpoint for each side', () => {
const r = rect(100, 100, 40, 20); // cx=120 cy=110
assert.deepEqual(anchor(r, 'left'), [100, 110]);
assert.deepEqual(anchor(r, 'right'), [140, 110]);
assert.deepEqual(anchor(r, 'top'), [120, 100]);
assert.deepEqual(anchor(r, 'bottom'), [120, 120]);
});
test('anchor falls back to the right edge for unknown/auto sides', () => {
const r = rect(100, 100, 40, 20);
assert.deepEqual(anchor(r, 'auto'), [140, 110]);
assert.deepEqual(anchor(r, undefined), [140, 110]);
});
test('defaultFromSide / defaultToSide are mirror pairs', () => {
const a = { cx: 0, cy: 0 };
const right = { cx: 100, cy: 0 };
assert.equal(defaultFromSide(a, right), 'right');
assert.equal(defaultToSide(a, right), 'left');
const below = { cx: 0, cy: 100 };
assert.equal(defaultFromSide(a, below), 'bottom');
assert.equal(defaultToSide(a, below), 'top');
});
test('chosenSide treats explicit "auto" as "use the geometric fallback"', () => {
assert.equal(chosenSide('left', 'right'), 'left');
assert.equal(chosenSide('auto', 'right'), 'right');
assert.equal(chosenSide(undefined, 'right'), 'right');
});
test('polylinePath emits M then L commands', () => {
assert.equal(polylinePath([[0, 0], [10, 0], [10, 10]]), 'M 0 0 L 10 0 L 10 10');
});
test('roundedPath degrades to a polyline for <3 points or radius<=0', () => {
assert.equal(roundedPath([[0, 0], [10, 0]], 10), 'M 0 0 L 10 0');
assert.equal(roundedPath([[0, 0], [10, 0], [10, 10]], 0), 'M 0 0 L 10 0 L 10 10');
});
test('roundedPath inserts a quadratic corner and never emits NaN', () => {
const d = roundedPath([[0, 0], [100, 0], [100, 100]], 10);
assert.match(d, /Q 100 0/); // corner pivots on the bend point
assert.doesNotMatch(d, /NaN/);
});
test('roundedPath clamps radius to half the shorter adjacent segment', () => {
// 6px segments with radius 10 → r clamps to 3; no overshoot / NaN.
const d = roundedPath([[0, 0], [6, 0], [6, 6]], 10);
assert.doesNotMatch(d, /NaN/);
assert.match(d, /^M 0 0/);
});
test('labelPoint: 2-point path is the midpoint lifted 10px, plus offsets', () => {
assert.deepEqual(labelPoint({}, [[0, 100], [100, 100]]), [50, 90]);
assert.deepEqual(labelPoint({ labelDx: 5, labelDy: -4 }, [[0, 100], [100, 100]]), [55, 86]);
});
test('labelPoint: labelSegment selects a segment and clamps to range', () => {
const pts = [[0, 0], [100, 0], [100, 100], [200, 100]];
// segment 0 → midpoint of pts[0],pts[1] = (50,0) lifted 10
assert.deepEqual(labelPoint({ labelSegment: 0 }, pts), [50, -10]);
// segment 99 clamps to the last segment
assert.deepEqual(labelPoint({ labelSegment: 99 }, pts), [150, 90]);
});
test('labelPoint: explicit labelAt wins outright', () => {
assert.deepEqual(labelPoint({ labelAt: [7, 8] }, [[0, 0], [100, 0]]), [7, 8]);
});
test('textUnits: ASCII=1, CJK=2, mixed sums, fullwidth supplementary=2', () => {
assert.equal(textUnits('abc'), 3);
assert.equal(textUnits('中文'), 4);
assert.equal(textUnits('a中'), 3);
assert.equal(textUnits(''), 0);
assert.equal(textUnits(null), 0);
assert.equal(textUnits('𠀀'), 2); // CJK Ext-B (supplementary plane)
assert.equal(textUnits('🚀'), 2); // emoji
assert.equal(textUnits('注入提示词'), 10); // issue #14 original label
assert.equal(textUnits('!@#012'), 12); // fullwidth punctuation + digits
});
test('textUnits follows wide and halfwidth East Asian presentation boundaries', () => {
assert.equal(textUnits('あカ'), 4); // Hiragana + Katakana are wide
assert.equal(textUnits('ㄅㆠ'), 4); // Bopomofo + extended Bopomofo are wide
assert.equal(textUnits('ㄱ'), 2); // Hangul compatibility letter is wide
assert.equal(textUnits('︐︙'), 4); // vertical punctuation forms are wide
assert.equal(textUnits('カタカナ'), 4); // halfwidth Katakana stays one unit per glyph
assert.equal(textUnits('ꥠ'), 2); // Hangul Jamo Extended-A is wide
});
test('textUnits counts emoji-presentation symbols in the BMP as wide', () => {
// These render at the same square advance as the supplementary-plane emoji,
// so counting them as one unit under-measures a label and lets it overflow
// its node while the layout receipt still reads clean.
assert.equal(textUnits('✅'), 2);
assert.equal(textUnits('⭐'), 2);
assert.equal(textUnits('⚡'), 2);
assert.equal(textUnits('⌛'), 2);
assert.equal(textUnits('⏰'), 2);
assert.equal(textUnits('⛔'), 2);
assert.equal(textUnits('❗'), 2);
assert.equal(textUnits('⬛'), 2);
assert.equal(textUnits('☕'), 2);
assert.equal(textUnits('♿'), 2);
assert.equal(textUnits('✅ Done'), 7);
// Narrow and ambiguous neighbours in the same blocks stay one unit.
assert.equal(textUnits('→'), 1); // rightwards arrow
assert.equal(textUnits('☎'), 1); // black telephone
assert.equal(textUnits('①'), 1); // circled digit one
// Unicode 16.0 moved these from Neutral to Wide.
assert.equal(textUnits('☰'), 2); // trigram for heaven
assert.equal(textUnits('☷'), 2); // trigram for earth
assert.equal(textUnits('⚊'), 2); // monogram for yang
assert.equal(textUnits('⚏'), 2); // digram for greater yin
// Hangul Jamo Extended-A stops at its last assigned jamo; the unassigned
// tail of the block defaults to Neutral.
assert.equal(textUnits('ꥼ'), 2);
assert.equal(textUnits('꥽'), 1);
});
test('textUnits measures a variation-selector sequence from the selector', () => {
// VS16 asks for emoji presentation: the pair renders as one square, so it
// must stay two units even though the base is now counted wide on its own.
assert.equal(textUnits('⭐️'), 2); // star
assert.equal(textUnits('✅️'), 2); // check mark button
assert.equal(textUnits('☕️'), 2); // hot beverage
assert.equal(textUnits('⚡️'), 2); // high voltage
// Same rule the other way: a narrow base forced to emoji presentation
// renders as a square and is two units, not one.
assert.equal(textUnits('✈️'), 2); // airplane
assert.equal(textUnits('❤️'), 2); // red heart
// VS15 asks for text presentation, which renders narrow.
assert.equal(textUnits('⭐︎'), 1);
assert.equal(textUnits('✈︎'), 1);
// The selector never adds width of its own, alone or in a run.
assert.equal(textUnits(''), 0);
assert.equal(textUnits('✅️ Done'), 7);
assert.equal(textUnits('⭐️⭐️'), 4);
});
test('semantic sigils cover every component and lifecycle kind without literal color', () => {
const kinds = [
'frontend', 'backend', 'database', 'cloud', 'security', 'messagebus', 'external',
'start', 'active', 'waiting', 'success', 'failure', 'neutral',
];
for (const kind of kinds) {
const sigil = renderSemanticSigil(kind, { x: 12, y: 18 });
assert.match(sigil, new RegExp(`data-semantic-sigil="${kind}"`), kind);
assert.match(sigil, /aria-hidden="true"/, kind);
assert.match(sigil, /class="semantic-sigil s-[a-z]+"/, kind);
assert.match(sigil, /transform="translate\(12 18\) scale\(0\.6875\)"/, kind);
assert.doesNotMatch(sigil, /#[0-9a-f]{3,8}|rgba?\(/i, kind);
}
});
test('unknown semantic sigils fail closed to a neutral role stamp', () => {
const sigil = renderSemanticSigil('vendor-logo', { x: 0, y: 0, size: 16 });
assert.match(sigil, /data-semantic-sigil="neutral"/);
assert.match(sigil, /class="semantic-sigil s-external"/);
assert.match(sigil, /scale\(1\)/);
});
test('suggestLabelObstacleFix includes rects and labelAt/labelDy hints', () => {
const labelRect = { x: 100, y: 180, width: 48, height: 14, label: '写入' };
const obstacle = { id: 'memtool', x: 30, y: 130, width: 230, height: 58 };
const hint = suggestLabelObstacleFix(labelRect, 124, 188, obstacle);
assert.match(hint, /label rect: \[100, 180, 48, 14\]/);
assert.match(hint, /component "memtool"/);
assert.match(hint, /Suggested fix: labelAt/);
assert.match(hint, /labelDy \+\d+/);
});
test('suggestComponentSeparation proposes nudged pos', () => {
const a = { id: 'api', x: 100, y: 200, width: 120, height: 60 };
const b = { id: 'db', x: 150, y: 200, width: 120, height: 60 };
const hint = suggestComponentSeparation(a, b, 8);
assert.match(hint, /move "db" pos to \[228, 200\]/);
});
test('applyTemplate preserves dollar sequences in titles', () => {
const template = `<html lang="en" data-theme="dark" data-preset="[VISUAL PRESET]">
<title>[PROJECT NAME] Architecture Diagram</title>
<h1>[PROJECT NAME] Architecture</h1>
<p class="subtitle">[Subtitle description]</p>
<!-- ARCHIFY:GUIDED_VIEWS_DATA -->
<!-- ARCHIFY:SVG_SLOT_START --><svg></svg> <!-- ARCHIFY:SVG_SLOT_END -->
<!-- ARCHIFY:CARDS_SLOT_START --><div></div> <!-- ARCHIFY:CARDS_SLOT_END -->`;
const html = applyTemplate(template, {
title: 'Plan $$50 tier',
subtitle: 'test',
svg: '<svg/>',
cards: '',
});
assert.match(html, /Plan \$\$50 tier/);
assert.match(html, /<p class="subtitle">test<\/p>/);
});
test('applyTemplate omits the subtitle row when no subtitle is authored', () => {
const template = `<html lang="en" data-theme="dark" data-preset="[VISUAL PRESET]">
<title>[PROJECT NAME] Architecture Diagram</title>
<h1>[PROJECT NAME] Architecture</h1>
<p class="subtitle">[Subtitle description]</p>
<!-- ARCHIFY:GUIDED_VIEWS_DATA -->
<!-- ARCHIFY:SVG_SLOT_START --><svg></svg> <!-- ARCHIFY:SVG_SLOT_END -->
<!-- ARCHIFY:CARDS_SLOT_START --><div></div> <!-- ARCHIFY:CARDS_SLOT_END -->`;
const html = applyTemplate(template, {
title: 'Focused title',
subtitle: ' ',
svg: '<svg/>',
cards: '',
});
assert.doesNotMatch(html, /class="subtitle"/);
assert.doesNotMatch(html, /Subtitle description/);
});
test('applyTemplate requires the new evidence slot only when evidence is present', () => {
const legacyTemplate = `<html lang="en" data-theme="dark" data-preset="[VISUAL PRESET]">
<title>[PROJECT NAME] Architecture Diagram</title>
<h1>[PROJECT NAME] Architecture</h1>
<p class="subtitle">[Subtitle description]</p>
<!-- ARCHIFY:GUIDED_VIEWS_DATA -->
<!-- ARCHIFY:SVG_SLOT_START --><svg></svg> <!-- ARCHIFY:SVG_SLOT_END -->
<!-- ARCHIFY:CARDS_SLOT_START --><div></div> <!-- ARCHIFY:CARDS_SLOT_END -->`;
assert.doesNotThrow(() => applyTemplate(legacyTemplate, {
title: 'Legacy', subtitle: '', svg: '<svg/>', cards: '',
}));
assert.throws(() => applyTemplate(legacyTemplate, {
title: 'Evidence', subtitle: '', svg: '<svg/>', cards: '',
sourceEvidence: { verified: true },
}), /repository evidence requires placeholder/);
});
+207
View File
@@ -0,0 +1,207 @@
// Golden-file harness for the archify renderers. No test framework needed:
// renderers are deterministic, so fresh renders must match both checked-in
// development and packaged example HTML aside from platform checkout line endings. Also covers schema enforcement (negative cases),
// template freshness of the architecture-mode example, and version sync.
//
// Run from the skill folder: npm test
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 repoRoot = path.resolve(skillRoot, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-test-'));
let failures = 0;
function check(name, ok, detail) {
if (ok) {
console.log(` ok ${name}`);
} else {
failures += 1;
console.error(` FAIL ${name}${detail ? `${detail}` : ''}`);
}
}
function render(mode, inputPath, outPath) {
execFileSync('node', [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
inputPath,
outPath,
], { stdio: ['ignore', 'ignore', 'pipe'] });
}
function normalizeNewlines(text) {
return text.replace(/\r\n?/g, '\n');
}
function shieldsBadgeMessages(source, label) {
const marker = `/badge/${label}-`;
const messages = [];
let searchFrom = 0;
while (searchFrom < source.length) {
const start = source.indexOf(marker, searchFrom);
if (start === -1) break;
let cursor = start + marker.length;
let message = '';
while (cursor < source.length) {
const character = source[cursor];
const next = source[cursor + 1];
if (character === '-' && next === '-') {
message += '-';
cursor += 2;
} else if (character === '_' && next === '_') {
message += '_';
cursor += 2;
} else if (character === '-') {
break;
} else {
message += character;
cursor += 1;
}
}
try { messages.push(decodeURIComponent(message)); } catch { messages.push(message); }
searchFrom = cursor + 1;
}
return messages;
}
// ---------------------------------------------------------------------------
console.log('golden renders (renderer output must match checked-in examples)');
const GOLDEN = [
['workflow', 'agent-tool-call.workflow.json', 'workflow-agent-tool-call-rendered.html'],
['sequence', 'cache-miss-request.sequence.json', 'sequence-cache-miss-request.html'],
['dataflow', 'product-analytics.dataflow.json', 'dataflow-product-analytics.html'],
['lifecycle', 'agent-run.lifecycle.json', 'lifecycle-agent-run.html'],
['architecture', 'web-app.architecture.json', 'web-app-rendered.html'],
];
for (const [mode, input, golden] of GOLDEN) {
const out = path.join(tmp, golden);
try {
render(mode, path.join(skillRoot, 'examples', input), out);
const fresh = fs.readFileSync(out, 'utf8');
const checked = fs.readFileSync(path.join(repoRoot, 'examples', golden), 'utf8');
const packaged = fs.readFileSync(path.join(skillRoot, 'examples', golden), 'utf8');
check(`${mode}: ${golden}`, normalizeNewlines(fresh) === normalizeNewlines(checked),
`fresh render differs from examples/${golden}; if the change is intentional, re-render the examples and commit them`);
check(`${mode}: packaged ${golden}`, normalizeNewlines(fresh) === normalizeNewlines(packaged),
`fresh render differs from archify/examples/${golden}; re-render the packaged examples and rebuild archify.zip`);
} catch (err) {
check(`${mode}: ${golden}`, false, String(err.stderr || err.message).slice(0, 300));
}
}
// ---------------------------------------------------------------------------
console.log('schema enforcement (invalid JSON must fail with a path-prefixed message)');
function expectFailure(name, mode, mutate, expectInMessage) {
const base = JSON.parse(fs.readFileSync(
path.join(skillRoot, 'examples', GOLDEN.find(([m]) => m === mode)[1]), 'utf8'));
mutate(base);
const input = path.join(tmp, `neg-${name.replace(/[^a-z0-9]+/gi, '-')}.json`);
fs.writeFileSync(input, JSON.stringify(base));
try {
render(mode, input, path.join(tmp, 'neg-out.html'));
check(name, false, 'renderer exited 0 on invalid input');
} catch (err) {
const message = String(err.stderr || err.message);
check(name, message.includes(expectInMessage),
`expected "${expectInMessage}" in:\n${message.slice(0, 300)}`);
}
}
expectFailure('card dot outside enum', 'workflow',
(d) => { d.cards[0].dot = 'pink'; }, '/cards/0/dot');
expectFailure('node id starting with a digit', 'workflow',
(d) => { d.nodes[0].id = '1user'; }, 'pattern');
expectFailure('extra property rejected', 'workflow',
(d) => { d.nodes[0].colour = 'red'; }, 'additional properties');
expectFailure('column beyond layout maximum', 'workflow',
(d) => { d.nodes[0].col = 7; }, '<= 5');
expectFailure('missing schema_version', 'sequence',
(d) => { delete d.schema_version; }, 'schema_version');
expectFailure('cross-lane state overlap', 'lifecycle',
(d) => {
const approval = d.states.find((s) => s.id === 'approval');
const failed = d.states.find((s) => s.id === 'failed');
delete failed.yOffset;
failed.col = approval.col;
}, 'less than 10px apart');
expectFailure('zero component width rejected by schema', 'architecture',
(d) => { d.components[0].size = [0, 60]; }, '/components/0/size/0');
expectFailure('zero component height rejected by schema', 'architecture',
(d) => { d.components[0].size = [120, 0]; }, '/components/0/size/1');
expectFailure('negative component width rejected by schema', 'architecture',
(d) => { d.components[0].size = [-1, 60]; }, '/components/0/size/0');
// ---------------------------------------------------------------------------
console.log('template freshness (architecture example must carry the current template)');
function blocks(html, tag) {
const re = new RegExp(`<${tag}[^>]*>[\\s\\S]*?<\\/${tag}>`, 'g');
return html.match(re) || [];
}
const template = fs.readFileSync(path.join(skillRoot, 'assets/template.html'), 'utf8');
const webApp = fs.readFileSync(path.join(repoRoot, 'examples/web-app.html'), 'utf8');
// <style> and <script> blocks pass through applyTemplate untouched, so the
// architecture-mode example must contain them verbatim or it has drifted.
for (const tag of ['style', 'script']) {
// The guided-view JSON script is generated from meta.views; compare only
// template-owned executable scripts, not per-diagram data payloads.
const isTemplateOwned = (block) => !block.includes('type="application/json"');
const t = blocks(template, tag).filter((b) => !b.includes('[PROJECT NAME]') && isTemplateOwned(b));
const w = blocks(webApp, tag).filter((b) => !b.includes('Sample Web App') && isTemplateOwned(b));
check(`web-app.html ${tag} blocks match template`,
JSON.stringify(t) === JSON.stringify(w),
'examples/web-app.html was generated from a stale template — re-derive it');
}
// ---------------------------------------------------------------------------
console.log('version sync');
const pkg = JSON.parse(fs.readFileSync(path.join(skillRoot, 'package.json'), 'utf8'));
check('template generator meta matches package.json version',
template.includes(`<meta name="generator" content="archify ${pkg.version}">`),
`package.json says ${pkg.version}`);
const lock = JSON.parse(fs.readFileSync(path.join(skillRoot, 'package-lock.json'), 'utf8'));
check('package-lock.json version matches package.json',
lock.version === pkg.version && lock.packages?.['']?.version === pkg.version,
`lockfile says ${lock.version} — run npm install and rebuild the zip`);
const skillMd = fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8');
const skillVersion = (skillMd.match(/^\s*version:\s*"([^"]+)"/m) || [])[1];
const packageMajorMinor = pkg.version.match(/^(\d+\.\d+)\./)?.[1];
check('SKILL.md metadata version matches package.json major.minor',
!!packageMajorMinor && skillVersion === packageMajorMinor,
`SKILL.md says ${skillVersion}, package.json says ${pkg.version}`);
for (const readmeName of ['README.md', 'README_EN.md', 'README_ZH.md']) {
const readme = fs.readFileSync(path.join(repoRoot, readmeName), 'utf8');
const badgeVersions = shieldsBadgeMessages(readme, 'version');
check(`${readmeName} badge matches package.json version`,
badgeVersions.length > 0 && badgeVersions.every((version) => version === pkg.version),
`${readmeName} badge says ${[...new Set(badgeVersions)].join(', ') || '(missing)'} instead of ${pkg.version}`);
}
const landingPage = fs.readFileSync(path.join(repoRoot, 'docs/index.html'), 'utf8');
const landingVersions = [...landingPage.matchAll(/\bv\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?\b/g)]
.map((match) => match[0]);
check('GitHub Pages version labels match package.json',
landingVersions.length > 0 && landingVersions.every((v) => v === `v${pkg.version}`),
`landing page says ${[...new Set(landingVersions)].join(', ') || '(no version)'}`);
// ---------------------------------------------------------------------------
fs.rmSync(tmp, { recursive: true, force: true });
if (failures) {
console.error(`\n${failures} check(s) failed`);
process.exit(1);
}
console.log('\nall checks passed');
+47
View File
@@ -0,0 +1,47 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { gridLayout, resolveComponentPos, validateGridPlacement } from '../renderers/architecture/grid.mjs';
test('gridLayout returns null for free placement', () => {
assert.equal(gridLayout({}), null);
assert.equal(gridLayout({ layout: undefined }), null);
});
test('resolveComponentPos prefers explicit pos over row/col', () => {
const grid = gridLayout({ layout: { mode: 'grid' } });
assert.deepEqual(resolveComponentPos({ pos: [9, 8], row: 0, col: 0 }, grid), [9, 8]);
});
test('resolveComponentPos maps row/col to pixel origin', () => {
const grid = gridLayout({
layout: { mode: 'grid', origin: [40, 80], gapX: 30, gapY: 40, cellW: 130, cellH: 64 },
});
assert.deepEqual(resolveComponentPos({ row: 0, col: 0 }, grid), [40, 80]);
assert.deepEqual(resolveComponentPos({ row: 1, col: 2 }, grid), [40 + 2 * 160, 80 + 104]);
});
test('validateGridPlacement rejects duplicate cells and missing row/col', () => {
const grid = gridLayout({ layout: { mode: 'grid', cols: 4 } });
const problems = [];
validateGridPlacement({
components: [
{ id: 'a', row: 0, col: 0 },
{ id: 'b', row: 0, col: 0 },
{ id: 'c' },
],
}, grid, problems);
assert.ok(problems.some((p) => p.includes('share grid cell')));
assert.ok(problems.some((p) => p.includes('"c" needs pos')));
});
test('validateGridPlacement ignores explicit pos overrides without row/col', () => {
const grid = gridLayout({ layout: { mode: 'grid', cols: 4 } });
const problems = [];
validateGridPlacement({
components: [
{ id: 'a', pos: [40, 80] },
{ id: 'b', pos: [200, 80] },
],
}, grid, problems);
assert.deepEqual(problems, []);
});
@@ -0,0 +1,59 @@
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 vm from 'node:vm';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(skillRoot, '..');
test('guide page: checked-in HTML is reproducible from the shared recipe source', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-guide-page-'));
const generated = path.join(tmp, 'guide.html');
try {
execFileSync(process.execPath, [path.join(repoRoot, 'scripts/build-guide.mjs'), generated]);
assert.equal(
fs.readFileSync(generated, 'utf8'),
fs.readFileSync(path.join(repoRoot, 'docs/guide.html'), 'utf8'),
);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test('guide page: ships bilingual recipes and syntactically valid interaction code', () => {
const html = fs.readFileSync(path.join(repoRoot, 'docs/guide.html'), 'utf8');
const packageVersion = JSON.parse(
fs.readFileSync(path.join(skillRoot, 'package.json'), 'utf8'),
).version;
const releaseIdentity = packageVersion.includes('-') ? 'development' : 'stable';
const staticVersionLabel = html.match(
/<span data-i18n="versionLabel">([^<]+)<\/span>/,
);
assert.doesNotMatch(html, /\[\[[A-Z0-9_]+\]\]/);
assert.equal(
staticVersionLabel?.[1],
`Scenario guide / ${releaseIdentity} / v${packageVersion}`,
);
assert.match(html, /Question-first diagramming/);
assert.match(html, /先问题,后图表/);
assert.match(html, /archify guide &quot;your scenario&quot;|archify guide "your scenario"/);
const dataMatch = html.match(/<script id="guide-data" type="application\/json">([\s\S]*?)<\/script>/);
assert.ok(dataMatch);
const data = JSON.parse(dataMatch[1]);
assert.equal(data.length, 11);
assert.equal(data.filter((recipe) => recipe.type === 'workflow').length, 3);
assert.ok(data.every((recipe) => recipe.en.prompt && recipe.zh.prompt && recipe.proof));
assert.match(html, /gallery\.html#proof-/);
assert.match(html, /Open verified example/);
assert.match(html, /打开验证成品/);
const scriptMatch = html.match(/<script>\n([\s\S]*?)\n <\/script>\n<\/body>/);
assert.ok(scriptMatch);
assert.doesNotThrow(() => new vm.Script(scriptMatch[1]));
});
@@ -0,0 +1,81 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
SCENARIO_RECIPES,
detectGuideLanguage,
listScenarioRecipes,
publicGuideData,
recommendScenario,
} from '../recipes/scenarios.mjs';
test('guide: exposes 11 unique recipes across every diagram type', () => {
assert.equal(SCENARIO_RECIPES.length, 11);
assert.equal(new Set(SCENARIO_RECIPES.map((recipe) => recipe.id)).size, 11);
assert.deepEqual(
Object.fromEntries(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'].map((type) => [
type,
SCENARIO_RECIPES.filter((recipe) => recipe.type === type).length,
])),
{ architecture: 2, workflow: 3, sequence: 2, dataflow: 2, lifecycle: 2 },
);
});
test('guide: every recipe has complete English and Chinese decision copy', () => {
for (const recipe of SCENARIO_RECIPES) {
assert.match(recipe.id, /^[a-z0-9]+(?:-[a-z0-9]+)*$/);
assert.ok(recipe.signals.length >= 8, recipe.id);
assert.ok(['classic', 'signal-flow', 'blueprint', 'editorial'].includes(recipe.presentation.preset), recipe.id);
for (const lang of ['en', 'zh']) {
const copy = recipe[lang];
assert.ok(copy.title.length >= 4, `${recipe.id}.${lang}.title`);
for (const field of ['question', 'summary', 'useWhen', 'avoidWhen', 'prompt']) {
assert.ok(copy[field].length > 10, `${recipe.id}.${lang}.${field}`);
}
assert.equal(copy.include.length, 4, `${recipe.id}.${lang}.include`);
}
}
});
test('guide: language detection and localization are deterministic', () => {
assert.equal(detectGuideLanguage('show an API request'), 'en');
assert.equal(detectGuideLanguage('展示 API 请求'), 'zh');
assert.equal(listScenarioRecipes('zh')[0].title, '系统总览');
assert.equal(listScenarioRecipes('en')[0].title, 'System overview');
});
test('guide: representative scenarios map to specialized recipes', () => {
const cases = [
['Show an API request with Redis cache miss', 'api-request'],
['Show CI/CD build deploy rollback', 'delivery-workflow'],
['展示 Kafka topic 消费者组和死信队列', 'event-stream'],
['梳理 ETL 数仓 PII 数据血缘', 'data-lineage'],
['deployment lifecycle approval rollback state', 'deployment-lifecycle'],
['agent tool call approval gate MCP', 'agent-tool-call'],
];
for (const [query, expected] of cases) {
assert.equal(recommendScenario(query).recommendation.id, expected, query);
}
});
test('guide: exact ids win and unknown questions fall back honestly', () => {
const exact = recommendScenario('incident-runbook');
assert.equal(exact.recommendation.id, 'incident-runbook');
assert.equal(exact.confidence, 'high');
const unknown = recommendScenario('make it delightful');
assert.equal(unknown.recommendation.id, 'system-overview');
assert.equal(unknown.confidence, 'low');
assert.deepEqual(unknown.matchedSignals, []);
});
test('guide: public data includes both languages and weighted signals', () => {
const data = publicGuideData();
assert.equal(data.length, 11);
for (const recipe of data) {
assert.ok(recipe.en.title);
assert.ok(recipe.zh.title);
assert.ok(recipe.proof, `${recipe.id}: verified proof is required`);
assert.ok(recipe.signals.every(([signal, weight]) => typeof signal === 'string' && weight > 0));
}
});
@@ -0,0 +1,144 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } 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-guided-views-'));
const CASES = {
architecture: { example: 'web-app.architecture.json', collection: 'components' },
workflow: { example: 'agent-tool-call.workflow.json', collection: 'nodes' },
sequence: { example: 'cache-miss-request.sequence.json', collection: 'participants' },
dataflow: { example: 'product-analytics.dataflow.json', collection: 'nodes' },
lifecycle: { example: 'agent-run.lifecycle.json', collection: 'states' },
};
function run(mode, doc, suffix) {
const input = path.join(tmp, `${mode}-${suffix}.json`);
const output = path.join(tmp, `${mode}-${suffix}.html`);
fs.writeFileSync(input, JSON.stringify(doc));
const result = spawnSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`), input, output,
], { encoding: 'utf8' });
return { result, output, html: fs.existsSync(output) ? fs.readFileSync(output, 'utf8') : '' };
}
function fixture(mode) {
return JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', CASES[mode].example), 'utf8'));
}
function svg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
for (const [mode, config] of Object.entries(CASES)) {
test(`${mode}: guided views preserve base SVG geometry`, () => {
const withViews = fixture(mode);
const ids = withViews[config.collection].slice(0, 2).map((item) => item.id);
withViews.meta.views = [{
id: 'reader-path',
label: 'Reader path',
focus: ids,
note: 'A safe note with </script><script> text.',
}];
const withoutViews = structuredClone(withViews);
delete withoutViews.meta.views;
const guided = run(mode, withViews, 'guided');
const plain = run(mode, withoutViews, 'plain');
assert.equal(guided.result.status, 0, guided.result.stderr);
assert.equal(plain.result.status, 0, plain.result.stderr);
assert.equal(svg(guided.html), svg(plain.html));
assert.match(guided.html, /id="guided-views" hidden/);
assert.match(guided.html, /Archify\.guidedViews = \(function \(\)/);
assert.match(guided.html, /#view=/);
assert.match(guided.html, /addEventListener\('hashchange', syncViewFromHash\)/);
assert.match(guided.html, /id="guided-view-play"/);
assert.match(guided.html, /VIEW_INTERVAL_MS = 3200/);
assert.match(guided.html, /visibilitychange/);
assert.match(guided.html, /play: startPlayback/);
assert.match(guided.html, /playCurrent: startCurrentViewPlayback/);
assert.match(guided.html, /URLSearchParams\(location\.search\)\.get\('play'\) === '1'/);
assert.match(guided.html, /data-autoplay/);
assert.match(guided.html, /prefers-reduced-motion: reduce/);
assert.match(guided.html, /pausePlayback\(\{ complete: true \}\)/);
assert.match(guided.html, /html\[data-embed="true"\] svg\[data-animation="trace"\] \[data-animate\],[\s\S]*?html\[data-share-playback="true"\][\s\S]*?animation: none !important;[\s\S]*?stroke-dashoffset: 0/);
assert.match(guided.html, /document\.documentElement\.setAttribute\('data-share-playback', 'true'\)/);
assert.match(guided.html, /document\.documentElement\.removeAttribute\('data-share-playback'\)/);
const oneShot = guided.html.match(/function startCurrentViewPlayback\(\) \{([\s\S]*?)\n function maybeStartSharePlayback/);
assert.ok(oneShot, 'one-shot share playback implementation missing');
assert.match(oneShot[1], /storyPlaybackScope = 'chapter'/);
assert.match(oneShot[1], /scheduleStoryPlayback\(\)/);
assert.doesNotMatch(oneShot[1], /scheduleNextView/);
assert.match(guided.html, /id="share-chapter-cue" hidden role="status" aria-live="polite"/);
assert.match(guided.html, /data-share-playback="true"\] \.share-chapter-cue:not\(\[hidden\]\)/);
assert.match(guided.html, /data-share-playback="true"\] \.diagram-container \{\s*padding-top: 4\.25rem/);
assert.match(guided.html, /function renderShareCue\(\)/);
assert.match(guided.html, /function shareCueBeatCopy\(state, view, stops\)/);
assert.match(guided.html, /viewerText\('viewer\.guided\.share\.step'/);
assert.match(guided.html, /shareCue\.setAttribute\('aria-live', state === 'playing' \? 'off' : 'polite'\)/);
assert.match(guided.html, /function scheduleStoryPlayback\(\)/);
assert.match(guided.html, /storyBeatTimer = setTimeout/);
assert.match(guided.html, /generation !== storyPlaybackGeneration/);
assert.match(guided.html, /settleStoryBeats\(\);/);
assert.match(guided.html, /kind === 'multiple' \? '\\u21c4' : '\\u00b7'/);
assert.match(guided.html, /currentStoryProgress\(\)/);
assert.match(guided.html, /setShareCueProgress\(progress\)/);
assert.match(guided.html, /startShareCueProgress\(progress, remainingChapter\)/);
assert.match(guided.html, /data-wide-diagram/);
assert.match(guided.html, /min-width: 720px/);
assert.match(guided.html, /reveal: reveal/);
assert.match(guided.html, /container\.addEventListener\('scroll', onScroll, \{ passive: true \}\)/);
assert.match(guided.html, /--archify-scroll-x/);
assert.match(guided.html, /focus: function \(\) \{ return activeIndex < 0 \? \[\] : views\[activeIndex\]\.focus\.slice\(\); \}/);
assert.doesNotMatch(guided.html, /<p class="footer">/);
assert.doesNotMatch(guided.html, /<kbd>P<\/kbd> play story/);
assert.doesNotMatch(plain.html, /<kbd>P<\/kbd> play story/);
assert.match(guided.html, /\\u003c\/script\\u003e\\u003cscript\\u003e/);
assert.doesNotMatch(guided.html, /A safe note with <\/script><script>/);
});
}
test('guided views reject duplicate view ids', () => {
const doc = fixture('workflow');
doc.meta.views = [
{ id: 'same', label: 'First', focus: ['user'] },
{ id: 'same', label: 'Second', focus: ['chat'] },
];
const { result } = run('workflow', doc, 'duplicate-view-id');
assert.notEqual(result.status, 0);
assert.match(result.stderr, /duplicates view id "same"/);
});
test('guided views reject dangling semantic ids', () => {
const doc = fixture('sequence');
doc.meta.views = [{ id: 'broken', label: 'Broken', focus: ['ghost'] }];
const { result } = run('sequence', doc, 'dangling-id');
assert.notEqual(result.status, 0);
assert.match(result.stderr, /references unknown semantic id "ghost"/);
});
test('guided views schema enforces collection and focus bounds', () => {
const tooMany = fixture('architecture');
tooMany.meta.views = Array.from({ length: 6 }, (_, index) => ({
id: `view-${index}`,
label: `View ${index}`,
focus: [tooMany.components[0].id],
}));
const overLimit = run('architecture', tooMany, 'too-many');
assert.notEqual(overLimit.result.status, 0);
assert.match(overLimit.result.stderr, /must NOT have more than 5 items/);
const duplicateFocus = fixture('dataflow');
duplicateFocus.meta.views = [{ id: 'duplicate', label: 'Duplicate', focus: ['web', 'web'] }];
const duplicate = run('dataflow', duplicateFocus, 'duplicate-focus');
assert.notEqual(duplicate.result.status, 0);
assert.match(duplicate.result.stderr, /duplicates semantic id "web"/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,37 @@
import { parse, parseFragment } from 'parse5';
import { SaxesParser } from 'saxes';
function visit(node, callback, insideSvg = false) {
callback(node, insideSvg);
const childInsideSvg = insideSvg || node.tagName === 'svg';
for (const child of node.childNodes || []) visit(child, callback, childInsideSvg);
if (node.content) visit(node.content, callback, childInsideSvg);
}
export function parseXml(source) {
return new SaxesParser({ xmlns: true }).write(source).close();
}
export function extractSvgs(markup, fragment = false) {
const document = fragment
? parseFragment(markup, { sourceCodeLocationInfo: true })
: parse(markup, { sourceCodeLocationInfo: true });
const direct = [];
const srcdocs = [];
visit(document, (node, insideSvg) => {
if (node.tagName === 'svg' && !insideSvg && node.sourceCodeLocation) {
direct.push(markup.slice(node.sourceCodeLocation.startOffset, node.sourceCodeLocation.endOffset));
}
const srcdoc = node.attrs?.find((attribute) => attribute.name === 'srcdoc');
if (srcdoc) srcdocs.push(srcdoc.value);
});
return {
direct,
embedded: srcdocs.flatMap((srcdoc) => {
const nested = extractSvgs(srcdoc, true);
return [...nested.direct, ...nested.embedded];
}),
};
}
+422
View File
@@ -0,0 +1,422 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { ChromeVisualBrowser, findChrome } from '../bin/visual-check.mjs';
import {
SUPPORTED_LOCALES,
catalogKeys,
translateCount,
translateMessage,
} from '../renderers/shared/i18n.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const cli = path.join(skillRoot, 'bin/archify.mjs');
const templatePath = path.join(skillRoot, 'assets/template.html');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-i18n-'));
const chromePath = process.env.ARCHIFY_CHROME ? findChrome() : null;
let sequence = 0;
const EXAMPLES = {
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 example(type) {
return JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', EXAMPLES[type]), 'utf8'));
}
const AUTHORED_TEXT_KEYS = new Set([
'title',
'subtitle',
'label',
'sublabel',
'tag',
'note',
'context',
'responsibility',
'classification',
'step',
]);
function authoredExample(type, locale) {
const document = example(type);
const authored = [];
let authoredIndex = 0;
const nextAuthoredText = () => {
authoredIndex += 1;
const value = locale === 'zh-CN'
? `文案${String(authoredIndex).padStart(2, '0')}`
: `Copy${String(authoredIndex).padStart(2, '0')}`;
authored.push(value);
return value;
};
const rewrite = (value, path = []) => {
if (Array.isArray(value)) {
value.forEach((item, index) => rewrite(item, [...path, index]));
return;
}
if (!value || typeof value !== 'object') return;
for (const [key, child] of Object.entries(value)) {
if (typeof child === 'string' && AUTHORED_TEXT_KEYS.has(key)) {
value[key] = nextAuthoredText();
} else if (key === 'items' && path.includes('cards') && Array.isArray(child)) {
value[key] = child.map((item) => (typeof item === 'string' ? nextAuthoredText() : item));
} else {
rewrite(child, [...path, key]);
}
}
};
rewrite(document);
document.meta.locale = locale;
if (!document.meta.subtitle) document.meta.subtitle = nextAuthoredText();
return { document, authored };
}
function run(type, document, command = 'render') {
const id = sequence++;
const input = path.join(tmp, `${id}-${type}.json`);
const output = path.join(tmp, `${id}-${type}.html`);
fs.writeFileSync(input, JSON.stringify(document));
const args = command === 'render'
? [cli, 'render', type, input, output]
: [cli, 'validate', type, input, '--json'];
const result = spawnSync(process.execPath, args, { cwd: skillRoot, encoding: 'utf8' });
return {
...result,
output,
html: result.status === 0 && command === 'render' ? fs.readFileSync(output, 'utf8') : '',
};
}
async function evaluate(browser, sessionId, expression, awaitPromise = false) {
const response = await browser.cdp.send('Runtime.evaluate', {
expression,
returnByValue: true,
awaitPromise,
}, sessionId);
if (response.exceptionDetails) {
throw new Error(response.exceptionDetails.exception?.description
|| response.exceptionDetails.text
|| 'browser evaluation failed');
}
return response.result?.value;
}
async function loadArtifact(browser, artifactPath) {
const sessionId = await browser.sessionPromise;
await browser.cdp.send('Emulation.setDeviceMetricsOverride', {
width: 1440,
height: 900,
deviceScaleFactor: 1,
mobile: false,
}, sessionId);
const loaded = browser.cdp.waitFor('Page.loadEventFired', sessionId);
const navigation = await browser.cdp.send('Page.navigate', {
url: pathToFileURL(artifactPath).href,
}, sessionId);
if (navigation.errorText) throw new Error(`Chrome navigation failed: ${navigation.errorText}`);
await loaded;
await evaluate(browser, sessionId, `new Promise(function (resolve) {
requestAnimationFrame(function () { requestAnimationFrame(function () { resolve(true); }); });
})`, true);
return sessionId;
}
test('zh-CN localizes renderer-owned output across all five modes without translating authored content', () => {
assert.deepEqual(SUPPORTED_LOCALES, ['en', 'zh-CN']);
for (const type of Object.keys(EXAMPLES)) {
const document = example(type);
const authoredTitle = document.meta.title;
document.meta.locale = 'zh-CN';
delete document.meta.subtitle;
const result = run(type, document);
assert.equal(result.status, 0, `${type}: ${result.stderr || result.stdout}`);
assert.match(result.html, /^<!DOCTYPE html>\n<html lang="zh-CN"/);
assert.match(result.html, /<svg\b[^>]*\blang="zh-CN"/);
assert.ok(result.html.includes(`<title>${authoredTitle}</title>`), `${type}: authored title changed`);
assert.ok(result.html.includes(`<h1>${authoredTitle}</h1>`), `${type}: authored heading changed`);
assert.match(result.html, /<text\b[^>]*>\u56fe\u4f8b<\/text>/);
assert.match(result.html, /aria-label="\u805a\u7126/);
assert.match(result.html, new RegExp(`<desc id="archify-diagram-description">\u7531 Archify \u751f\u6210\u7684`));
assert.match(result.html, /"locale":"zh-CN"/);
assert.match(result.html, />\u5bfc\u51fa\u56fe\u8868</);
assert.doesNotMatch(result.html, /\{\{i18n:/);
}
});
test('explicit en and zh-CN preserve complete authored field inventories across all five modes', () => {
for (const type of Object.keys(EXAMPLES)) {
const english = authoredExample(type, 'en');
const chinese = authoredExample(type, 'zh-CN');
assert.equal(english.authored.length, chinese.authored.length, `${type}: authored shapes differ`);
assert.ok(english.authored.length >= 10, `${type}: authored inventory is unexpectedly small`);
if (type === 'dataflow') {
assert.ok(
english.authored.includes(english.document.flows[0].classification),
'dataflow: classification is missing from the authored inventory',
);
}
if (type === 'lifecycle') {
assert.ok(
english.authored.includes(english.document.states[0].step),
'lifecycle: step is missing from the authored inventory',
);
}
for (const candidate of [english, chinese]) {
const locale = candidate.document.meta.locale;
const result = run(type, candidate.document);
assert.equal(result.status, 0, `${type}/${locale}: ${result.stderr || result.stdout}`);
assert.match(result.html, new RegExp(`^<!DOCTYPE html>\\n<html lang="${locale}"`));
assert.match(result.html, new RegExp(`<svg\\b[^>]*\\blang="${locale}"`));
assert.match(result.html, new RegExp(`"locale":"${locale}"`));
for (const authoredText of candidate.authored) {
assert.ok(result.html.includes(authoredText), `${type}/${locale}: lost authored text ${authoredText}`);
}
if (locale === 'zh-CN') {
assert.ok(result.html.includes(`<title>${candidate.document.meta.title}</title>`), type);
assert.match(result.html, />导出图表</);
} else {
assert.ok(result.html.includes(`<title>${candidate.document.meta.title} Diagram</title>`), type);
assert.match(result.html, />Export diagram</);
}
}
}
});
test('omitted locale preserves non-English authored content and the English Viewer contract in all five modes', () => {
for (const type of Object.keys(EXAMPLES)) {
const document = example(type);
const authoredTitle = `作者内容-${type}`;
document.meta.title = authoredTitle;
delete document.meta.locale;
delete document.meta.subtitle;
const result = run(type, document);
assert.equal(result.status, 0, `${type}: ${result.stderr || result.stdout}`);
assert.match(result.html, /^<!DOCTYPE html>\n<html lang="en"/);
assert.ok(result.html.includes(`<title>${authoredTitle} Diagram</title>`), `${type}: authored title changed`);
assert.ok(result.html.includes(`<h1>${authoredTitle}</h1>`), `${type}: authored heading changed`);
assert.match(result.html, /<svg\b[^>]*\blang="en"/);
assert.match(result.html, /aria-label="Focus /);
assert.match(result.html, /"locale":"en"/);
assert.match(result.html, />Export diagram</);
}
});
test('unsupported locale values fail schema validation in every mode', () => {
for (const locale of ['fr', 'zh-HK']) {
for (const type of Object.keys(EXAMPLES)) {
const document = example(type);
document.meta.locale = locale;
const result = run(type, document, 'validate');
assert.notEqual(result.status, 0, `${type}: unsupported locale ${locale} unexpectedly passed`);
const payload = JSON.parse(result.stdout);
assert.equal(payload.ok, false);
assert.ok(payload.diagnostics.some((entry) => entry.subject?.path === '/meta/locale'), `${type}: ${locale}`);
}
}
});
test('real Chrome keeps zh-CN Finder, Route, Export, and accessibility UI localized in all five modes', {
skip: chromePath ? false : 'Set ARCHIFY_CHROME to run the real browser localization regression.',
}, async () => {
const browser = new ChromeVisualBrowser(chromePath);
try {
for (const type of Object.keys(EXAMPLES)) {
const document = example(type);
document.meta.locale = 'zh-CN';
document.meta.title = `浏览器本地化-${type}`;
const result = run(type, document);
assert.equal(result.status, 0, `${type}: ${result.stderr || result.stdout}`);
const sessionId = await loadArtifact(browser, result.output);
const state = await evaluate(browser, sessionId, `(function () {
var finderButton = document.getElementById('btn-node-finder');
var routeButton = document.getElementById('btn-route-probe');
var exportButton = document.getElementById('btn-export');
finderButton.click();
var finder = {
hidden: document.getElementById('node-finder').hidden,
title: document.getElementById('node-finder-title').textContent.trim(),
searchLabel: document.getElementById('node-finder-input').getAttribute('aria-label')
};
document.getElementById('node-finder-close').click();
routeButton.click();
var route = {
hidden: document.getElementById('route-probe').hidden,
title: document.getElementById('route-probe-title').textContent.trim(),
label: routeButton.getAttribute('aria-label')
};
routeButton.click();
exportButton.click();
var exportMenu = document.getElementById('export-menu');
function pseudoContent(selector) {
var content = getComputedStyle(document.querySelector(selector), '::after').content || '';
return content.replace(/^["']|["']$/g, '');
}
var presetBadges = {};
['signal-flow', 'blueprint', 'editorial'].forEach(function (preset) {
document.documentElement.setAttribute('data-preset', preset);
presetBadges[preset] = {
header: pseudoContent('.header-row'),
plate: pseudoContent('.diagram-container')
};
});
return {
htmlLang: document.documentElement.lang,
svgLang: document.querySelector('.diagram-container svg').getAttribute('lang'),
toolbarLabel: document.querySelector('.diagram-nav').getAttribute('aria-label'),
finder: finder,
route: route,
exportMenuOpen: exportMenu.classList.contains('open'),
exportLabel: exportButton.getAttribute('aria-label'),
exportMenuLabel: exportMenu.getAttribute('aria-label'),
exportMenuText: exportMenu.textContent,
presetBadges: presetBadges
};
})()`);
assert.equal(state.htmlLang, 'zh-CN', type);
assert.equal(state.svgLang, 'zh-CN', type);
assert.equal(state.toolbarLabel, '图表视图控制', type);
assert.deepEqual(state.finder, {
hidden: false,
title: '查找节点',
searchLabel: '搜索图表节点',
}, type);
assert.deepEqual(state.route, {
hidden: false,
title: '选择起点节点',
label: '清除已追踪路径',
}, type);
assert.equal(state.exportMenuOpen, true, type);
assert.equal(state.exportLabel, '导出图表', type);
assert.equal(state.exportMenuLabel, '导出', type);
assert.match(state.exportMenuText, /分享卡片/, type);
assert.deepEqual(state.presetBadges, {
'signal-flow': { header: '信号流', plate: 'none' },
blueprint: { header: '蓝图 / 修订 01', plate: '' },
editorial: { header: '编辑风格 / 现场笔记', plate: 'ARCHIFY / 图版 04' },
}, type);
const shareCardFailure = await evaluate(browser, sessionId, `(async function () {
var originalGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function () { return null; };
try {
await Archify.exportMenu.shareCard();
return { rejected: false, message: '' };
} catch (error) {
return { rejected: true, message: String(error && error.message || error) };
} finally {
HTMLCanvasElement.prototype.getContext = originalGetContext;
}
})()`, true);
assert.deepEqual(shareCardFailure, {
rejected: true,
message: '无法为分享卡片创建二维画布上下文',
}, type);
const visual = spawnSync(process.execPath, [cli, 'visual-check', result.output, '--json'], {
cwd: skillRoot,
encoding: 'utf8',
env: { ...process.env, ARCHIFY_CHROME: chromePath },
});
assert.ok([0, 1].includes(visual.status), `${type}: ${visual.stderr || visual.stdout}`);
const receipt = JSON.parse(visual.stdout);
assert.equal(receipt.visualReview, 'pending', type);
assert.equal(receipt.chrome.status, 'available', type);
assert.equal(receipt.readability.status, 'pass', type);
assert.equal(receipt.viewerChrome.status, 'pass', type);
assert.equal(receipt.captures.status, 'pass', type);
assert.equal(
receipt.containment.viewports.every((viewport) => viewport.overflowX === false),
true,
`${type}: localized Viewer introduced horizontal overflow`,
);
}
} finally {
await browser.close();
}
});
test('every Viewer message reference resolves through the shared catalog', () => {
const template = fs.readFileSync(templatePath, 'utf8');
const keys = new Set(catalogKeys());
const references = new Set([
...[...template.matchAll(/\{\{i18n:([a-zA-Z0-9_.-]+)\}\}/g)].map((match) => match[1]),
...[...template.matchAll(/['"](viewer\.[a-zA-Z0-9_.-]+)['"]/g)].map((match) => match[1]),
]);
const unresolved = [...references].filter((key) => (
!key.endsWith('.') && !keys.has(key) && !(keys.has(`${key}.one`) && keys.has(`${key}.other`))
));
assert.deepEqual(unresolved, []);
});
test('every supported catalog is complete and preserves interpolation variables', () => {
const variables = (value) => [...value.matchAll(/\{([a-zA-Z0-9_]+)\}/g)]
.map((match) => match[1])
.sort();
for (const key of catalogKeys()) {
const expected = variables(translateMessage('en', key));
for (const locale of SUPPORTED_LOCALES) {
const message = translateMessage(locale, key);
assert.ok(message && message !== 'undefined', `${locale}: ${key}`);
assert.deepEqual(variables(message), expected, `${locale}: ${key}`);
}
}
});
test('runtime labels stay localized after composition', () => {
assert.equal(translateMessage('zh-CN', 'viewer.kind.backend'), '后端');
assert.equal(translateMessage('zh-CN', 'viewer.kind.decision'), '决策');
assert.equal(translateMessage('zh-CN', 'viewer.passport.relationship.connectsFrom'), '连接自');
assert.equal(translateMessage('zh-CN', 'viewer.nav.level.auto'), '自动');
const zhHops = translateCount('zh-CN', 'viewer.route.hop', 2);
assert.equal(
translateMessage('zh-CN', 'viewer.finder.result.routeTarget', { label: '终点', links: zhHops }),
'选择终点作为路径终点,2 跳',
);
const enHop = translateCount('en', 'viewer.route.overview.hop', 1);
const enNode = translateCount('en', 'viewer.route.overview.node', 2);
assert.equal(
translateMessage('en', 'viewer.route.overview.status', { nodes: enNode, hops: enHop }),
'2 nodes · 1 directed hop · shortest authored route',
);
});
test('Share Card and export failures use catalog messages instead of fixed English', () => {
assert.equal(
translateCount('zh-CN', 'viewer.export.card.routeSummary', 2, { source: '来源', target: '目标' }),
'路径:来源 → 目标 · 2 个有向跳转',
);
assert.equal(
translateMessage('zh-CN', 'viewer.export.error.toBlobNull', { label: '分享卡片' }),
'分享卡片的 canvas.toBlob 未返回数据',
);
const template = fs.readFileSync(templatePath, 'utf8');
for (const hardcoded of [
"'Route: '",
"'Share Card variants cannot be combined'",
"canvas2dOrThrow(canvas, 'Share Card')",
"'Share Card export could not remove temporary viewer state'",
"'WebM motion export requires a trace animation and browser MediaRecorder support'",
]) {
assert.ok(!template.includes(hardcoded), hardcoded);
}
});
@@ -0,0 +1,111 @@
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-intent-trace-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all typed renderers inherit one geometry-neutral Intent Trace', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /Archify\.intentTrace = \(function \(\)/, mode);
assert.match(html, /id="intent-trace-status" role="status" aria-live="polite" aria-atomic="true"/, mode);
assert.match(html, /svg\.setAttribute\('data-intent-trace-active', id\)/, mode);
assert.match(html, /data-intent-trace-overlay/, mode);
assert.equal((html.match(/<svg\b/g) || []).length, 1, `${mode} keeps one static canonical SVG`);
assert.doesNotMatch(canonicalSvg(html), /data-intent-trace|intent-trace-flow/, mode);
}
});
test('Intent Trace derives exact one-hop direction from stable renderer relationships', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /function show\(id, options\)/);
assert.match(html, /if \(from !== id && to !== id\) return/);
assert.match(html, /direction = from === id && to === id \? 'loop' : \(from === id \? 'out' : 'in'\)/);
assert.match(html, /related\[from\] = true/);
assert.match(html, /related\[to\] = true/);
assert.match(html, /edge\.setAttribute\('data-intent-trace-match', ''\)/);
assert.match(html, /node\.setAttribute\('data-intent-trace-selected', ''\)/);
assert.match(html, /clone\.setAttribute\('data-direction', direction\)/);
assert.match(html, /counts\[direction\] \+= 1/);
});
test('Intent Trace keeps incoming and outgoing motion on authored source-to-target geometry', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
const incomingRule = html.match(/\.intent-trace-flow\[data-direction="in"\]\s*\{[\s\S]*?\}/)?.[0] || '';
assert.ok(incomingRule, `${mode}: expected the incoming Intent Trace style`);
assert.doesNotMatch(
incomingRule,
/animation-direction\s*:\s*reverse/,
`${mode}: incoming authored geometry must not be replayed from target to source`,
);
assert.match(incomingRule, /animation-direction\s*:\s*normal/, mode);
assert.match(html, /function traceGeometry\(shape, direction\)[\s\S]+shape\.cloneNode\(false\)/, mode);
assert.match(html, /@keyframes archify-intent-trace-flow[\s\S]+stroke-dashoffset: -1/, mode);
}
});
test('Intent Trace separates hover, keyboard, touch, and committed focus', () => {
const html = render('sequence', CASES.sequence);
assert.match(html, /window\.matchMedia\('\(hover: hover\) and \(pointer: fine\)'\)/);
assert.match(html, /event\.pointerType === 'touch'/);
assert.match(html, /addEventListener\('pointerover'/);
assert.match(html, /addEventListener\('pointerout'/);
assert.match(html, /addEventListener\('focusin'/);
assert.match(html, /addEventListener\('focusout'/);
assert.match(html, /show\(node\.getAttribute\('data-node-id'\), \{ announce: true \}\)/);
assert.match(html, /Press Enter for details/);
assert.match(html, /html\.getAttribute\('data-embed'\) === 'true'/);
assert.match(html, /container\.classList\.contains\('is-panning'\)/);
assert.match(html, /svg\.hasAttribute\('data-story-active'\)/);
assert.match(html, /svg\.hasAttribute\('data-relationship-preview-active'\)/);
assert.match(html, /Archify\.focus\.active\(\)/);
assert.match(html, /Archify\.intentTrace\.clear\(\{ announce: false \}\)/);
assert.match(html, /e\.key === 'Escape' && Archify\.intentTrace\.active\(\)/);
});
test('Intent Trace normalizes motion, respects reduced motion, and exports cleanly', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /clone\.setAttribute\('pathLength', '1'\)/);
assert.match(html, /\.intent-trace-flow\[data-direction="out"\]/);
assert.match(html, /\.intent-trace-flow\[data-direction="in"\]/);
assert.match(html, /\.intent-trace-flow\[data-direction="loop"\]/);
assert.match(html, /@keyframes archify-intent-trace-flow/);
assert.match(html, /animation: archify-intent-trace-flow 1\.15s linear 1 both/);
assert.match(html, /@media \(prefers-reduced-motion: reduce\)[\s\S]+\.intent-trace-flow \{[\s\S]+animation: none !important/);
assert.match(html, /clone\.removeAttribute\('data-intent-trace-active'\)/);
assert.match(html, /clone\.querySelectorAll\('\[data-intent-trace-overlay\]'\)/);
assert.match(html, /clone\.querySelectorAll\('\[data-intent-trace-match\], \[data-intent-trace-selected\]'\)/);
assert.match(html, /!clone\.hasAttribute\('data-intent-trace-active'\)/);
assert.doesNotMatch(canonicalSvg(html), /data-intent-trace|intent-trace-flow/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,97 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(skillRoot, '..');
const docsRoot = path.join(repoRoot, 'docs');
const landing = fs.readFileSync(path.join(docsRoot, 'index.html'), 'utf8');
const manifest = JSON.parse(fs.readFileSync(path.join(docsRoot, 'gallery', 'manifest.json'), 'utf8'));
const proofs = [
{
key: 'signal', id: 'agent-tool-call', artifact: 'gallery/artifacts/agent-tool-call.workflow.html',
preset: 'signal-flow', nodes: 12, edges: 11, view: 'happy-path',
},
{
key: 'blueprint', id: 'deployment-ownership', artifact: 'gallery/artifacts/production-deployment.architecture.html',
preset: 'blueprint', nodes: 12, edges: 12, view: 'request-boundary',
},
{
key: 'classic', id: 'cache-miss', artifact: 'gallery/artifacts/cache-miss.sequence.html',
preset: 'classic', nodes: 7, edges: 12, view: 'cache-fallback',
},
];
test('landing metadata describes the full technical-diagram product and trusted hero promise', () => {
assert.match(landing, /<title>Archify — Technical Diagrams from Plain English<\/title>/);
assert.match(landing, /<meta property="og:title" content="Archify — Technical Diagrams from Plain English">/);
assert.match(landing, /An agent skill for Cursor, Claude Code, Codex CLI, and OpenCode/);
assert.equal((landing.match(/npx -y skills add tt-a1i\/archify --skill archify --agent cursor --global --copy --yes/g) || []).length, 2);
assert.match(landing, /From plain English<br>to architecture <em>you can trust\.<\/em>/);
});
test('landing hero leads with three real generated proof artifacts', () => {
assert.match(landing, /id="hero-proof-stage"/);
assert.match(landing, /id="hero-proof-panel" role="tabpanel"/);
assert.equal((landing.match(/class="spec-card"/g) || []).length, 3);
assert.equal((landing.match(/role="tab"/g) || []).length, 3);
assert.doesNotMatch(landing, /class="hero-screenshot/);
for (const proof of proofs) {
const entry = manifest.entries.find(item => item.id === proof.id);
assert.ok(entry, `${proof.id}: proof manifest entry missing`);
assert.equal(entry.artifact, proof.artifact);
assert.equal(entry.visualPreset, proof.preset);
assert.equal(entry.animation, 'trace');
assert.equal(entry.nodeCount, proof.nodes);
assert.equal(entry.edgeCount, proof.edges);
assert.ok(entry.viewIds.includes(proof.view));
assert.ok(entry.checks.every(check => check.ok), `${proof.id}: validation receipt is not green`);
assert.ok(fs.existsSync(path.join(docsRoot, proof.artifact)), `${proof.id}: live artifact missing`);
assert.match(landing, new RegExp(`data-proof="${proof.key}"`));
assert.ok(landing.includes(`artifact: '${proof.artifact}'`));
assert.ok(landing.includes(`view: '${proof.view}'`));
}
});
test('landing proof switcher is bilingual and keyboard navigable', () => {
assert.match(landing, /proof-live':'Live proof'/);
assert.match(landing, /proof-live':'实时成品'/);
assert.match(landing, /event\.key === 'ArrowRight'/);
assert.match(landing, /event\.key === 'ArrowLeft'/);
assert.match(landing, /event\.key === 'Home'/);
assert.match(landing, /event\.key === 'End'/);
assert.match(landing, /proofFrame\.dataset\.proof !== key/);
assert.match(landing, /\?embed=1&amp;play=1&amp;theme=dark#view=happy-path/);
assert.match(landing, /sandbox="allow-scripts"/);
assert.match(landing, /const playback = play \? '&play=1' : ''/);
assert.match(landing, /proofEmbedUrl\(proof, \{ play: deliberate \}\)/);
assert.doesNotMatch(landing, /proofFrame\.contentWindow|proofFrame\.contentDocument/);
assert.match(landing, /\?present=1&play=1#view=/);
assert.match(landing, /#view=\$\{encodeURIComponent\(proof\.view\)\}/);
assert.match(landing, /Pin one exact Story Moment, copy its stable link, and let someone else open the same authored node/);
});
test('landing makes Route Journey and core exploration shortcuts discoverable in both languages', () => {
assert.match(landing, /Route Journey keeps the complete authored path visible/);
assert.match(landing, /Route Journey 始终保留完整作者路径/);
assert.match(landing, /one finite, reader-controlled pass over each exact incoming relationship/);
assert.match(landing, /沿每条精确入向关系播放一次由读者控制的有限旅程/);
assert.match(landing, /data-i18n="f7-tag">INSPECT · PLAY · PAUSE/);
assert.match(landing, /'f7-tag':'INSPECT · PLAY · PAUSE'/);
assert.match(landing, /'f7-tag':'检查 · 播放 · 暂停'/);
assert.match(landing, /data-i18n="kbd-zoom">Reading depth \/ reset/);
assert.match(landing, /'kbd-zoom':'阅读层级 \/ 复位'/);
assert.match(landing, /data-i18n="kbd-guide">Diagram guide<\/span><kbd>\?<\/kbd>/);
assert.match(landing, /'kbd-guide':'图表指南'/);
assert.match(landing, /data-i18n="kbd-find">Find node \/ route endpoint<\/span><kbd>\/<\/kbd>/);
assert.match(landing, /'kbd-find':'查找节点 \/ 路径端点'/);
assert.match(landing, /data-i18n="kbd-route">Trace, inspect, and play a route<\/span><kbd>R<\/kbd>/);
assert.match(landing, /'kbd-route':'探查、检查并播放路径'/);
assert.match(landing, /data-i18n="kbd-lens">Compare semantic kinds<\/span><kbd>L<\/kbd>/);
assert.match(landing, /'kbd-lens':'对比语义类型'/);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,404 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } 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 cli = path.join(skillRoot, 'bin/archify.mjs');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-legend-contract-'));
let sequence = 0;
const FIXTURES = {
architecture: {
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Legend architecture', viewBox: [720, 420] },
components: [
{ id: 'ui', type: 'frontend', label: 'UI', pos: [60, 90] },
{ id: 'store', type: 'database', label: 'Store', pos: [300, 90] },
],
connections: [],
},
workflow: {
schema_version: 1,
diagram_type: 'workflow',
meta: { title: 'Legend workflow', viewBox: [720, 360] },
lanes: [{ id: 'main', label: 'Main' }],
nodes: [
{ id: 'ui', lane: 'main', col: 0, type: 'frontend', label: 'UI' },
{ id: 'agent', lane: 'main', col: 2, type: 'backend', label: 'Agent' },
],
edges: [],
},
sequence: {
schema_version: 1,
diagram_type: 'sequence',
meta: { title: 'Legend sequence', viewBox: [720, 560] },
participants: [
{ id: 'client', type: 'frontend', label: 'Client' },
{ id: 'api', type: 'backend', label: 'API' },
],
messages: [
{ from: 'client', to: 'api', y: 220, label: 'request', variant: 'emphasis' },
{ from: 'api', to: 'client', y: 280, label: 'response', variant: 'return' },
],
},
dataflow: {
schema_version: 1,
diagram_type: 'dataflow',
meta: { title: 'Default Flow Only' },
stages: [{ label: 'Input' }, { label: 'Output' }],
nodes: [
{ id: 'input', type: 'backend', label: 'Input', stage: 0, row: 0 },
{ id: 'output', type: 'backend', label: 'Output', stage: 1, row: 0 },
],
flows: [
{ from: 'input', to: 'output', label: 'request', route: 'straight' },
],
},
lifecycle: {
schema_version: 1,
diagram_type: 'lifecycle',
meta: { title: 'No Waiting or Failure', viewBox: [720, 566] },
lanes: [{ id: 'main', label: 'Lifecycle' }],
states: [
{ id: 'started', type: 'start', label: 'Started', lane: 'main', col: 0 },
{ id: 'running', type: 'active', label: 'Running', lane: 'main', col: 1 },
{ id: 'completed', type: 'success', label: 'Completed', lane: 'main', col: 2 },
],
transitions: [
{ from: 'started', to: 'running' },
{ from: 'running', to: 'completed' },
],
},
};
const CATALOGS = {
architecture: ['frontend', 'backend', 'database', 'cloud', 'security', 'messagebus', 'external'],
workflow: ['frontend', 'backend', 'security', 'messagebus', 'database', 'cloud', 'external'],
sequence: ['emphasis', 'return', 'security', 'dashed', 'default'],
dataflow: ['emphasis', 'security', 'dashed', 'database', 'default'],
lifecycle: ['start', 'active', 'waiting', 'decision', 'success', 'failure', 'neutral', 'external'],
};
const AUTO_KINDS = {
architecture: ['frontend', 'database'],
workflow: ['frontend', 'backend'],
sequence: ['emphasis', 'return'],
dataflow: ['default'],
lifecycle: ['start', 'active', 'success'],
};
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function withLegend(type, legend) {
const doc = clone(FIXTURES[type]);
if (legend !== undefined) doc.meta.legend = legend;
return doc;
}
function run(type, doc, command = 'render') {
const id = sequence++;
const input = path.join(tmp, `${id}-${type}.json`);
const output = path.join(tmp, `${id}-${type}.html`);
fs.writeFileSync(input, JSON.stringify(doc));
const args = command === 'render'
? [cli, 'render', type, input, output]
: [cli, 'validate', type, input, '--json'];
const result = spawnSync(process.execPath, args, { cwd: skillRoot, encoding: 'utf8' });
return {
...result,
html: result.status === 0 && command === 'render' ? fs.readFileSync(output, 'utf8') : '',
};
}
function render(type, doc) {
const result = run(type, doc);
assert.equal(result.status, 0, result.stderr || result.stdout);
return result.html;
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
function attrValues(source, attribute) {
const pattern = new RegExp(`${attribute}="([^"]+)"`, 'g');
return [...source.matchAll(pattern)].map((match) => match[1]);
}
function legendKinds(html) {
return attrValues(canonicalSvg(html), 'data-legend-semantic-kind');
}
function validateFailure(type, doc) {
const result = run(type, doc, 'validate');
assert.notEqual(result.status, 0, `expected ${type} validation to fail`);
const payload = JSON.parse(result.stdout);
assert.equal(payload.ok, false);
return payload;
}
test('public typed renderers default to auto and expose only authored semantic kinds', () => {
for (const type of Object.keys(FIXTURES)) {
const html = render(type, FIXTURES[type]);
assert.deepEqual(legendKinds(html), AUTO_KINDS[type], type);
}
});
test('Dataflow database node facts are interactive while flow variants stay visual-only', () => {
const withDatabase = clone(FIXTURES.dataflow);
withDatabase.nodes[1].type = 'database';
const databaseSvg = canonicalSvg(render('dataflow', withDatabase));
assert.deepEqual(attrValues(databaseSvg, 'data-legend-semantic-kind'), ['database', 'default']);
assert.deepEqual(attrValues(databaseSvg, 'data-legend-kind'), ['database']);
assert.equal((databaseSvg.match(/data-legend-bridge=""/g) || []).length, 1);
assert.ok(attrValues(databaseSvg, 'data-node-kind').includes('database'));
const forcedWithoutFact = canonicalSvg(render('dataflow', withLegend('dataflow', {
entries: { database: { visible: true } },
})));
assert.deepEqual(attrValues(forcedWithoutFact, 'data-legend-semantic-kind'), ['database', 'default']);
assert.deepEqual(attrValues(forcedWithoutFact, 'data-legend-kind'), []);
assert.doesNotMatch(forcedWithoutFact, /data-legend-bridge/);
});
test('Issue #52 dataflow and lifecycle reproductions publish truthful default legends', () => {
const dataflow = canonicalSvg(render('dataflow', FIXTURES.dataflow));
assert.deepEqual(attrValues(dataflow, 'data-legend-semantic-kind'), ['default']);
assert.doesNotMatch(dataflow, /policy \/ PII|async batch|primary data|data store/i);
assert.doesNotMatch(dataflow, /data-legend-bridge|data-legend-kind=/);
const lifecycle = canonicalSvg(render('lifecycle', FIXTURES.lifecycle));
const lifecycleLegend = lifecycle.slice(lifecycle.indexOf('<!-- Legend -->'));
assert.deepEqual(attrValues(lifecycleLegend, 'data-legend-semantic-kind'), ['start', 'active', 'success']);
assert.doesNotMatch(lifecycleLegend, /waiting|failure \/ exit/i);
assert.deepEqual(attrValues(lifecycleLegend, 'data-legend-kind'), ['start', 'active', 'success']);
});
test('all mode follows each renderer-owned stable catalog order', () => {
for (const type of Object.keys(FIXTURES)) {
const html = render(type, withLegend(type, { mode: 'all' }));
assert.deepEqual(legendKinds(html), CATALOGS[type], type);
}
});
test('hidden mode removes the complete legend and overrides visible true', () => {
for (const type of Object.keys(FIXTURES)) {
const forcedKind = CATALOGS[type].at(-1);
const html = render(type, withLegend(type, {
mode: 'hidden',
entries: { [forcedKind]: { label: 'Must stay hidden', visible: true } },
}));
const svg = canonicalSvg(html);
assert.doesNotMatch(svg, />Legend</);
assert.doesNotMatch(svg, /data-legend(?:-semantic-kind|-kind|-bridge)?=/);
assert.doesNotMatch(svg, /Must stay hidden/);
}
});
test('visibility overrides apply after auto/all and empty legends leave no chrome', () => {
const cases = {
architecture: { hidden: 'frontend', forced: 'external' },
workflow: { hidden: 'frontend', forced: 'security' },
sequence: { hidden: 'emphasis', forced: 'dashed' },
dataflow: { hidden: 'default', forced: 'database' },
lifecycle: { hidden: 'active', forced: 'waiting' },
};
for (const [type, kinds] of Object.entries(cases)) {
const html = render(type, withLegend(type, {
entries: {
[kinds.hidden]: { visible: false },
[kinds.forced]: { visible: true },
},
}));
const expected = AUTO_KINDS[type]
.filter((kind) => kind !== kinds.hidden)
.concat(kinds.forced)
.sort((left, right) => CATALOGS[type].indexOf(left) - CATALOGS[type].indexOf(right));
assert.deepEqual(legendKinds(html), expected, type);
}
const allMinusSecurity = render('sequence', withLegend('sequence', {
mode: 'all',
entries: { security: { visible: false } },
}));
assert.deepEqual(
legendKinds(allMinusSecurity),
CATALOGS.sequence.filter((kind) => kind !== 'security'),
);
for (const type of Object.keys(FIXTURES)) {
const entries = Object.fromEntries(AUTO_KINDS[type].map((kind) => [kind, { visible: false }]));
const empty = canonicalSvg(render(type, withLegend(type, { entries })));
assert.doesNotMatch(empty, />Legend</, type);
assert.doesNotMatch(empty, /data-legend/, type);
}
});
test('label overrides round-trip through all five public renderers', () => {
for (const type of Object.keys(FIXTURES)) {
const kind = AUTO_KINDS[type][0];
const label = `Custom ${type} label`;
const svg = canonicalSvg(render(type, withLegend(type, {
entries: { [kind]: { label } },
})));
assert.match(svg, new RegExp(`data-legend-semantic-kind="${kind}"`), type);
assert.match(svg, new RegExp(`>${label}<`), type);
if (['architecture', 'workflow', 'lifecycle'].includes(type)) {
assert.match(svg, new RegExp(`data-legend-kind="${kind}"[^>]+data-legend-label="${label}"`), type);
} else {
assert.doesNotMatch(svg, /data-legend-kind=/, type);
}
}
});
test('label overrides preserve stable kinds and exact Semantic Legend boundaries', () => {
const architecture = render('architecture', withLegend('architecture', {
entries: {
frontend: { label: 'Reader <UI> & "ops"' },
external: { label: 'Future integration', visible: true },
},
}));
const svg = canonicalSvg(architecture);
const baselineSvg = canonicalSvg(render('architecture', FIXTURES.architecture));
assert.deepEqual(attrValues(svg, 'data-node-id'), attrValues(baselineSvg, 'data-node-id'));
assert.deepEqual(attrValues(svg, 'data-node-kind'), attrValues(baselineSvg, 'data-node-kind'));
assert.deepEqual(attrValues(svg, 'data-edge-from'), attrValues(baselineSvg, 'data-edge-from'));
assert.match(svg, />Reader &lt;UI&gt; &amp; &quot;ops&quot;</);
assert.doesNotMatch(svg, /<UI>/);
assert.match(svg, />Future integration</);
assert.deepEqual(attrValues(svg, 'data-legend-semantic-kind'), ['frontend', 'database', 'external']);
assert.deepEqual(attrValues(svg, 'data-legend-kind'), ['frontend', 'database']);
assert.match(svg, /data-legend-kind="frontend"[^>]+data-legend-label="Reader &lt;UI&gt; &amp; &quot;ops&quot;"/);
assert.match(architecture, /entry\.getAttribute\('data-legend-label'\)/);
for (const type of ['sequence', 'dataflow']) {
const kind = type === 'sequence' ? 'emphasis' : 'default';
const html = canonicalSvg(render(type, withLegend(type, {
entries: { [kind]: { label: 'Visible only' } },
})));
assert.match(html, />Visible only</);
assert.doesNotMatch(html, /data-legend-bridge|data-legend-kind=/);
}
});
test('strict per-renderer schemas reject malformed legend contracts with path-prefixed errors', () => {
const known = {
architecture: 'frontend', workflow: 'frontend', sequence: 'default', dataflow: 'default', lifecycle: 'start',
};
const cases = [
[{ mode: 'sometimes' }, '/meta/legend/mode'],
[{ entries: { unknown_kind: { visible: true } } }, '/meta/legend/entries'],
[(type) => ({ entries: { [known[type]]: { label: '' } } }), '/meta/legend/entries/'],
[(type) => ({ entries: { [known[type]]: { visible: 'yes' } } }), '/meta/legend/entries/'],
[(type) => ({ entries: { [known[type]]: { color: '#fff' } } }), '/meta/legend/entries/'],
[{ mode: 'auto', extra: true }, '/meta/legend'],
];
for (const type of Object.keys(FIXTURES)) {
for (const [legendOrFactory, pathPrefix] of cases) {
const legend = typeof legendOrFactory === 'function' ? legendOrFactory(type) : legendOrFactory;
const failure = validateFailure(type, withLegend(type, legend));
assert.ok(
failure.diagnostics.some((diagnostic) => diagnostic.subject.path.startsWith(pathPrefix)),
`${type}: expected a diagnostic under ${pathPrefix}: ${JSON.stringify(failure.diagnostics)}`,
);
}
}
});
test('measured legends fail explicitly instead of wrapping into diagram content', () => {
const label = '界'.repeat(40);
const entries = Object.fromEntries(CATALOGS.workflow.map((kind) => [kind, { label }]));
const failure = validateFailure('workflow', withLegend('workflow', { mode: 'all', entries }));
const diagnostic = failure.diagnostics.find((entry) => entry.code === 'legend/vertical-overflow');
assert.ok(diagnostic, JSON.stringify(failure.diagnostics));
assert.equal(diagnostic.subject.path, '/meta/legend');
assert.ok(diagnostic.evidence.rowCount > 1);
const routedEntries = Object.fromEntries(CATALOGS.architecture.map((kind) => [
kind,
{ label: `Long ${kind} convention` },
]));
const routed = withLegend('architecture', { mode: 'all', entries: routedEntries });
routed.connections = [{
from: 'ui',
to: 'store',
label: 'bottom route',
fromSide: 'bottom',
toSide: 'bottom',
via: [[120, 382], [360, 382]],
labelAt: [240, 370],
}];
const renderFailure = run('architecture', routed);
assert.notEqual(renderFailure.status, 0);
assert.match(renderFailure.stderr, /legend\/content-overlap/);
assert.equal(renderFailure.html, '');
});
test('explicit Architecture viewBox rejects legend title rectangles that overlap content', () => {
const doc = clone(FIXTURES.architecture);
doc.meta.viewBox = [320, 320];
doc.meta.legend = { mode: 'auto' };
doc.components = [
{ id: 'ui', type: 'frontend', label: 'UI', pos: [40, 216], size: [120, 60] },
];
const failure = validateFailure('architecture', doc);
const diagnostic = failure.diagnostics.find((entry) => entry.code === 'legend/vertical-overflow');
assert.ok(diagnostic, JSON.stringify(failure.diagnostics));
assert.equal(diagnostic.subject.path, '/meta/legend');
assert.ok(diagnostic.evidence.requiredTopY < diagnostic.evidence.availableTopY);
});
test('a single unfit label fails with a path-specific width diagnostic', () => {
const failure = validateFailure('architecture', withLegend('architecture', {
entries: { frontend: { label: '界'.repeat(80) } },
}));
const diagnostic = failure.diagnostics.find((entry) => entry.code === 'legend/label-too-wide');
assert.ok(diagnostic, JSON.stringify(failure.diagnostics));
assert.equal(diagnostic.subject.path, '/meta/legend/entries/frontend/label');
assert.ok(diagnostic.evidence.measuredWidthPx > diagnostic.evidence.availableWidthPx);
});
test('measured legend rows share baselines and stay within the viewBox for localized labels', () => {
const doc = withLegend('lifecycle', {
mode: 'all',
entries: {
start: { label: '开始 / Start of the complete lifecycle' },
active: { label: '正在执行 active processing' },
waiting: { label: '等待人工输入' },
decision: { label: 'Decision gate with deterministic wrapping' },
success: { label: '成功完成' },
failure: { label: 'Failure / 失败' },
neutral: { label: 'Neutral state' },
external: { label: 'External system' },
},
});
const svg = canonicalSvg(render('lifecycle', doc));
const viewBox = attrValues(svg.match(/<svg\b[^>]*>/)?.[0] || '', 'viewBox')[0].split(/\s+/).map(Number);
const tags = [...svg.matchAll(/<g\b[^>]*data-legend-semantic-kind="[^"]+"[^>]*>/g)].map((match) => match[0]);
assert.equal(tags.length, CATALOGS.lifecycle.length);
const boxes = tags.map((tag) => ({
x: Number(attrValues(tag, 'data-legend-x')[0]),
y: Number(attrValues(tag, 'data-legend-baseline')[0]),
width: Number(attrValues(tag, 'data-legend-width')[0]),
}));
assert.ok(boxes.every((box) => Number.isFinite(box.x) && Number.isFinite(box.y) && Number.isFinite(box.width)));
assert.ok(boxes.every((box) => box.x >= 0 && box.x + box.width <= viewBox[2]));
const rows = new Map();
for (const box of boxes) rows.set(box.y, [...(rows.get(box.y) || []), box]);
for (const row of rows.values()) {
const sorted = [...row].sort((left, right) => left.x - right.x);
for (let index = 1; index < sorted.length; index += 1) {
assert.ok(sorted[index - 1].x + sorted[index - 1].width <= sorted[index].x);
}
}
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,111 @@
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-motion-governor-'));
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, example, animation = 'trace') {
const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', example), 'utf8'));
doc.meta = { ...doc.meta };
if (animation) doc.meta.animation = animation;
else delete doc.meta.animation;
const input = path.join(tmp, `${mode}-${animation || 'static'}.json`);
const output = path.join(tmp, `${mode}-${animation || 'static'}.html`);
fs.writeFileSync(input, JSON.stringify(doc));
execFileSync('node', [path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`), input, output], {
stdio: ['ignore', 'ignore', 'pipe'],
});
return fs.readFileSync(output, 'utf8');
}
function svgBlock(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all renderers inherit one viewer-only Live/Still Motion Governor', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /id="btn-motion"[^>]+hidden[^>]+aria-label="Pause motion"/, mode);
assert.match(html, /Archify\.motionGovernor = \(function \(\)/, mode);
assert.match(html, /var capable = !!\(svg && svg\.getAttribute\('data-animation'\) === 'trace'\)/, mode);
assert.match(svgBlock(html), /data-animation="trace"/, mode);
assert.doesNotMatch(svgBlock(html), /data-motion-(?:capable|owner)|motion-control/, mode);
}
});
test('static artifacts stay truly still while trace artifacts opt into ambient motion', () => {
const html = render('architecture', CASES.architecture, null);
const svg = svgBlock(html);
assert.doesNotMatch(svg, /data-animation=/);
assert.match(html, /\.pulse-dot \{[\s\S]*?animation: none;/);
assert.match(html, /html\[data-motion-capable="true"\] \.pulse-dot \{ animation: pulse 2s infinite; \}/);
assert.match(html, /html\[data-motion-capable="true"\]\[data-preset="signal-flow"\]\[data-ambient-motion="running"\] \.diagram-container::before/);
assert.match(html, /if \(!capable\) \{[\s\S]*?btn\.hidden = true;[\s\S]*?capable: false/);
assert.match(html, /html\.setAttribute\('data-motion-capable', 'true'\);[\s\S]*?btn\.hidden = false/);
});
test('reader pause is persistent, explicit, and reduced-motion aware', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /var STORAGE_KEY = 'archify-motion'/);
assert.match(html, /localStorage\.setItem\(STORAGE_KEY, 'still'\)/);
assert.match(html, /localStorage\.removeItem\(STORAGE_KEY\)/);
assert.match(html, /html\.setAttribute\('data-motion', paused \? 'still' : 'live'\)/);
assert.match(html, /btn\.setAttribute\('aria-pressed', paused \? 'false' : 'true'\)/);
assert.match(html, /Motion paused by reduced-motion preference/);
assert.match(html, /motionQuery\.addEventListener\('change', render\)/);
assert.match(html, /document\.addEventListener\('visibilitychange', syncVisibility\)/);
assert.match(html, /Archify\.guidedViews\.isPlaying\(\)[\s\S]*?Archify\.guidedViews\.pause\(\)/);
assert.match(html, /play\.disabled = !playing && !automaticPlaybackAllowed/);
assert.match(html, /Story playback unavailable while motion is Still/);
assert.match(html, /\.pulse-dot \{ animation: none !important; \}/);
assert.match(html, /html\[data-motion="still"\] \.story-trail-flow/);
});
test('strong semantic intent receives the single motion budget', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /if \(svg\.hasAttribute\('data-story-playing'\) \|\| svg\.hasAttribute\('data-story-follow'\)\) return 'story'/);
assert.match(html, /if \(svg\.hasAttribute\('data-story-active'\)\) return 'chapter'/);
assert.match(html, /data-route-picking'[\s\S]*?return 'route'/);
assert.match(html, /data-lens-active'\)\) return 'lens'/);
assert.match(html, /data-relationship-preview-active'\)\) return 'relationship'/);
assert.match(html, /data-intent-trace-active'\)\) return 'intent'/);
assert.match(html, /data-focus-active'\)\) return 'focus'/);
assert.match(html, /data-legend-preview-active'\)\) return 'legend'/);
assert.match(html, /new MutationObserver\(function \(\) \{ publishOwner\(\); \}\)/);
assert.match(html, /html\[data-motion-owner\] svg\[data-animation="trace"\] \[data-animate\]/);
assert.match(html, /function claim\(next, cleanup\)[\s\S]*?return ownerToken/);
assert.match(html, /function release\(token\)[\s\S]*?token !== ownerToken/);
assert.match(html, /function clearClaim\(preempted\)[\s\S]*?try \{ cleanup\(\); \} catch/);
assert.match(html, /function claim\(next, cleanup\)[\s\S]*?clearClaim\(true\)/);
assert.match(html, /animation: archify-route-probe-flow 1\.1s[\s\S]*?1 both/);
assert.doesNotMatch(html, /archify-route-probe-flow[^;]*infinite/);
});
test('motion control is mobile-contained, embed-safe, and export-neutral', () => {
const html = render('sequence', CASES.sequence);
assert.match(html, /\.toolbar #btn-motion\[hidden\] \{ display: none !important; \}/);
assert.match(html, /\.toolbar button \{[\s\S]*?min-height: 2\.75rem;/);
assert.match(html, /@media \(max-width: 360px\) \{[\s\S]*?\.toolbar #btn-motion \{ min-width: 4\.4rem;/);
assert.match(html, /#theme-label, #preset-label, #present-label \{ display: none; \}/);
assert.match(html, /html\[data-embed="true"\] \.diagram-container::before/);
assert.match(html, /html\[data-share-playback="true"\] \.diagram-container::before/);
assert.match(html, /Still also parks bounded[\s\S]*?viewer signals without discarding their static meaning/);
assert.match(html, /recordExportReceipt\('svg', blob, d\.canonicalStateClean\)/);
assert.doesNotMatch(svgBlock(html), /btn-motion|data-motion=|data-motion-owner/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,118 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import path from 'node:path';
import { openArtifact, openLoopbackUrl } from '../bin/open-artifact.mjs';
const target = path.resolve("/tmp/-复杂 path 'quoted'/diagram.html");
test('open artifact: uses argument arrays without shell interpolation on every supported platform', () => {
const cases = [
{
platform: 'darwin',
command: 'open',
args: [target],
method: 'open',
},
{
platform: 'linux',
command: 'xdg-open',
args: [target],
method: 'xdg-open',
},
{
platform: 'win32',
command: 'powershell.exe',
args: [
'-NoProfile',
'-NonInteractive',
'-Command',
'Start-Process -FilePath $args[0]',
target,
],
method: 'powershell',
},
];
for (const expected of cases) {
let invocation;
const result = openArtifact(target, {
platform: expected.platform,
spawn(command, args, options) {
invocation = { command, args, options };
return { status: 0 };
},
});
assert.deepEqual(result, {
requested: true,
status: 'opened',
target,
method: expected.method,
});
assert.equal(invocation.command, expected.command);
assert.deepEqual(invocation.args, expected.args);
assert.equal(invocation.options.shell, false);
assert.equal(invocation.options.timeout, 5000);
}
});
test('open artifact: distinguishes missing support from opener execution failure', () => {
const missing = openArtifact(target, {
platform: 'linux',
spawn() {
return { error: Object.assign(new Error('missing'), { code: 'ENOENT' }) };
},
});
assert.equal(missing.status, 'unsupported');
assert.equal(missing.method, 'xdg-open');
const timedOut = openArtifact(target, {
platform: 'darwin',
spawn() {
return { error: Object.assign(new Error('timed out'), { code: 'ETIMEDOUT' }) };
},
});
assert.equal(timedOut.status, 'failed');
assert.equal(timedOut.method, 'open');
const unknown = openArtifact(target, { platform: 'plan9' });
assert.deepEqual(unknown, {
requested: true,
status: 'unsupported',
target,
method: null,
});
});
test('open artifact: live preview opens only an exact loopback HTTP root', () => {
const url = 'http://127.0.0.1:43127/';
let invocation;
const result = openLoopbackUrl(url, {
platform: 'darwin',
spawn(command, args, options) {
invocation = { command, args, options };
return { status: 0 };
},
});
assert.deepEqual(result, {
requested: true,
status: 'opened',
target: url,
method: 'open',
});
assert.deepEqual(invocation.args, [url]);
assert.equal(invocation.options.shell, false);
for (const rejected of [
'https://127.0.0.1:43127/',
'http://localhost:43127/',
'http://0.0.0.0:43127/',
'http://127.0.0.1:43127/path',
'http://127.0.0.1:43127/?source=secret',
'not a url',
]) {
assert.throws(() => openLoopbackUrl(rejected), /loopback|valid/i, rejected);
}
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,587 @@
import { spawn, spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import assert from 'node:assert/strict';
import { startPreview } from '../bin/preview.mjs';
import { loadDiagram, writeDiagram } from '../renderers/shared/cli.mjs';
import { pathsAlias } from '../renderers/shared/output-path.mjs';
const here = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(here, '..');
const cli = path.join(skillRoot, 'bin/archify.mjs');
const workflowFixture = path.join(skillRoot, 'examples/agent-tool-call.workflow.json');
const baseFixture = path.join(skillRoot, 'examples/checkout-platform.base.architecture.json');
const headFixture = path.join(skillRoot, 'examples/checkout-platform.head.architecture.json');
function run(args, cwd) {
return spawnSync(process.execPath, [cli, ...args], {
cwd,
encoding: 'utf8',
});
}
function copyInstalledSkill(target) {
fs.cpSync(skillRoot, target, {
recursive: true,
filter(source) {
const relative = path.relative(skillRoot, source);
return relative !== 'node_modules'
&& !relative.startsWith(`node_modules${path.sep}`)
&& relative !== 'test'
&& !relative.startsWith(`test${path.sep}`);
},
});
}
function directoryAliasesNames(directory, authoredName, lookupName) {
const authoredPath = path.join(directory, authoredName);
const lookupPath = path.join(directory, lookupName);
fs.writeFileSync(authoredPath, 'filesystem semantics probe', { flag: 'wx' });
try {
let authored;
let lookup;
try {
authored = fs.statSync(authoredPath);
lookup = fs.statSync(lookupPath);
} catch (error) {
if (error.code === 'ENOENT') return false;
throw error;
}
return authored.dev === lookup.dev && authored.ino === lookup.ino;
} finally {
fs.unlinkSync(authoredPath);
}
}
test('future-path aliases follow the containing directory case and Unicode semantics', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-semantics-'));
const caseInsensitive = directoryAliasesNames(cwd, 'ArchifyCaseProbe', 'archifycaseprobe');
const normalizationInsensitive = directoryAliasesNames(
cwd,
'archify-norm-\u00e9-probe',
'archify-norm-e\u0301-probe',
);
assert.equal(
pathsAlias(path.join(cwd, 'Future.HTML'), path.join(cwd, 'future.html')),
caseInsensitive,
);
assert.equal(
pathsAlias(path.join(cwd, 'Caf\u00e9.html'), path.join(cwd, 'Cafe\u0301.html')),
normalizationInsensitive,
);
});
test('compare rejects case-only future targets before input work when the directory aliases case', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-compare-case-'));
const caseInsensitive = directoryAliasesNames(cwd, 'ArchifyCaseProbe', 'archifycaseprobe');
const output = path.join(cwd, 'Future.HTML');
const receiptPath = path.join(cwd, 'future.html');
const result = run([
'compare', 'architecture',
path.join(cwd, 'missing-base.json'),
path.join(cwd, 'missing-head.json'),
output,
'--receipt', receiptPath,
'--json',
], cwd);
assert.equal(result.status, 1);
const receipt = JSON.parse(result.stdout);
assert.equal(
receipt.diagnostics[0].code,
caseInsensitive ? 'output/target-alias' : 'delta/base-input',
);
assert.equal(receipt.stage, caseInsensitive ? 'prepare' : 'input');
});
test('render reports an output symlink cycle as a structured output diagnostic', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-cycle-'));
const input = path.join(cwd, 'diagram.workflow.json');
const output = path.join(cwd, 'cycle-a.html');
const otherLink = path.join(cwd, 'cycle-b.html');
fs.copyFileSync(workflowFixture, input);
fs.symlinkSync(otherLink, output, 'file');
fs.symlinkSync(output, otherLink, 'file');
const result = spawnSync(
process.execPath,
[path.join(skillRoot, 'renderers/workflow/render-workflow.mjs'), input, output],
{
cwd,
encoding: 'utf8',
env: { ...process.env, ARCHIFY_DIAGNOSTIC_FORMAT: 'json' },
},
);
assert.equal(result.status, 1);
const failure = JSON.parse(result.stderr);
assert.equal(failure.diagnostics[0].code, 'output/symlink-cycle');
assert.equal(failure.diagnostics[0].subject.output, output);
assert.ok(failure.diagnostics[0].supportedFixes.length > 0);
});
test('render rejects an output symlink that aliases its JSON input', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-render-'));
const input = path.join(cwd, 'diagram.workflow.json');
const output = path.join(cwd, 'diagram.html');
const source = fs.readFileSync(workflowFixture);
fs.writeFileSync(input, source);
fs.symlinkSync(input, output, 'file');
const result = run(['render', 'workflow', input, output], cwd);
assert.equal(result.status, 1);
assert.match(result.stderr, /output must not replace an input/i);
assert.deepEqual(fs.readFileSync(input), source);
});
test('render rejects an existing output hard link to its JSON input', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-render-hardlink-'));
const input = path.join(cwd, 'diagram.workflow.json');
const output = path.join(cwd, 'diagram.html');
const source = fs.readFileSync(workflowFixture);
fs.writeFileSync(input, source);
fs.linkSync(input, output);
const result = run(['render', 'workflow', input, output], cwd);
assert.equal(result.status, 1);
assert.match(result.stderr, /output must not replace an input/i);
assert.deepEqual(fs.readFileSync(input), source);
});
test('render rejects an absolute meta.output when no CLI output is provided', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-meta-absolute-'));
const input = path.join(cwd, 'diagram.workflow.json');
const output = path.join(cwd, 'authored.html');
const source = JSON.parse(fs.readFileSync(workflowFixture, 'utf8'));
source.meta.output = output;
fs.writeFileSync(input, JSON.stringify(source));
const result = run(['render', 'workflow', input], cwd);
assert.equal(result.status, 1);
assert.match(result.stderr, /meta\.output must be a relative path/i);
assert.equal(fs.existsSync(output), false);
});
test('render rejects a relative meta.output that escapes the working directory', () => {
const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-meta-parent-'));
const cwd = path.join(parent, 'work');
fs.mkdirSync(cwd);
const input = path.join(cwd, 'diagram.workflow.json');
const output = path.join(parent, 'escaped.html');
const source = JSON.parse(fs.readFileSync(workflowFixture, 'utf8'));
source.meta.output = '../escaped.html';
fs.writeFileSync(input, JSON.stringify(source));
const result = run(['render', 'workflow', input], cwd);
assert.equal(result.status, 1);
assert.match(result.stderr, /meta\.output must stay inside the current working directory/i);
assert.equal(fs.existsSync(output), false);
});
test('render rejects a meta.output that escapes through a directory symlink', () => {
const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-meta-link-'));
const cwd = path.join(parent, 'work');
const outside = path.join(parent, 'outside');
fs.mkdirSync(cwd);
fs.mkdirSync(outside);
fs.symlinkSync(outside, path.join(cwd, 'linked'), 'dir');
const input = path.join(cwd, 'diagram.workflow.json');
const output = path.join(outside, 'authored.html');
const source = JSON.parse(fs.readFileSync(workflowFixture, 'utf8'));
source.meta.output = 'linked/authored.html';
fs.writeFileSync(input, JSON.stringify(source));
const result = run(['render', 'workflow', input], cwd);
assert.equal(result.status, 1);
assert.match(result.stderr, /meta\.output must stay inside the current working directory/i);
assert.equal(fs.existsSync(output), false);
});
test('render requires a meta.output target with an html extension', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-meta-extension-'));
const input = path.join(cwd, 'diagram.workflow.json');
const output = path.join(cwd, 'authored.json');
const source = JSON.parse(fs.readFileSync(workflowFixture, 'utf8'));
source.meta.output = 'authored.json';
fs.writeFileSync(input, JSON.stringify(source));
const result = run(['render', 'workflow', input], cwd);
assert.equal(result.status, 1);
assert.match(result.stderr, /meta\.output must target an? \.html file/i);
assert.equal(fs.existsSync(output), false);
});
test('render rejects a meta.output symlink that resolves to a non-html target', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-meta-extension-link-'));
const input = path.join(cwd, 'diagram.workflow.json');
const target = path.join(cwd, 'authored.json');
const output = path.join(cwd, 'authored.html');
const source = JSON.parse(fs.readFileSync(workflowFixture, 'utf8'));
source.meta.output = 'authored.html';
fs.writeFileSync(input, JSON.stringify(source));
fs.writeFileSync(target, 'trusted target');
fs.symlinkSync(target, output, 'file');
const result = run(['render', 'workflow', input], cwd);
assert.equal(result.status, 1);
assert.match(result.stderr, /meta\.output must resolve to an? \.html file/i);
assert.equal(fs.readFileSync(target, 'utf8'), 'trusted target');
});
test('deliver rejects a future-path alias of its JSON input with a structured diagnostic', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-deliver-'));
const realDirectory = path.join(cwd, 'real');
const linkedDirectory = path.join(cwd, 'linked');
fs.mkdirSync(realDirectory);
fs.symlinkSync(realDirectory, linkedDirectory, 'dir');
const input = path.join(realDirectory, 'diagram.workflow.json');
const output = path.join(linkedDirectory, 'diagram.workflow.json');
const source = fs.readFileSync(workflowFixture);
fs.writeFileSync(input, source);
const result = run(['deliver', 'workflow', input, output, '--json'], cwd);
assert.equal(result.status, 1);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.stage, 'prepare');
assert.equal(receipt.diagnostics[0].code, 'output/input-alias');
assert.deepEqual(fs.readFileSync(input), source);
});
test('deliver rechecks aliases immediately before committing a verified candidate', { timeout: 10000 }, async () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-deliver-race-'));
const installedRoot = path.join(cwd, 'skill');
const installedBin = path.join(installedRoot, 'bin');
const installedShared = path.join(installedRoot, 'renderers/shared');
const installedRenderer = path.join(installedRoot, 'renderers/workflow');
const installedScripts = path.join(installedRoot, 'scripts');
fs.mkdirSync(installedBin, { recursive: true });
fs.mkdirSync(installedShared, { recursive: true });
fs.mkdirSync(installedRenderer, { recursive: true });
fs.mkdirSync(installedScripts, { recursive: true });
fs.copyFileSync(cli, path.join(installedBin, 'archify.mjs'));
fs.copyFileSync(
path.join(skillRoot, 'renderers/shared/output-path.mjs'),
path.join(installedShared, 'output-path.mjs'),
);
fs.writeFileSync(path.join(installedRenderer, 'render-workflow.mjs'), `
import fs from 'node:fs';
const [, output] = process.argv.slice(2);
fs.writeFileSync(process.env.ARCHIFY_TEST_RENDER_STARTED, output);
await new Promise((resolve) => setTimeout(resolve, 500));
fs.writeFileSync(output, '<!doctype html><title>verified candidate</title><svg></svg>');
`);
fs.writeFileSync(path.join(installedScripts, 'check-render-output.mjs'), `
console.log(JSON.stringify({
ok: true,
checks: [{ name: 'single_svg', ok: true }],
composition: {
profile: 'showcase',
status: 'pass',
summary: { errors: 0, warnings: 0 }
}
}));
`);
const inputDirectory = path.join(cwd, 'input');
const initialOutputDirectory = path.join(cwd, 'safe-output');
const linkedDirectory = path.join(cwd, 'linked-output');
fs.mkdirSync(inputDirectory);
fs.mkdirSync(initialOutputDirectory);
fs.symlinkSync(initialOutputDirectory, linkedDirectory, 'dir');
const input = path.join(inputDirectory, 'diagram.json');
const output = path.join(linkedDirectory, 'diagram.json');
const source = Buffer.from('{"meta":{"title":"race input"}}');
fs.writeFileSync(input, source);
const marker = path.join(cwd, 'renderer-started');
const child = spawn(process.execPath, [
path.join(installedBin, 'archify.mjs'),
'deliver', 'workflow', input, output, '--json',
], {
cwd,
encoding: 'utf8',
env: { ...process.env, ARCHIFY_TEST_RENDER_STARTED: marker },
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
const started = Date.now();
while (!fs.existsSync(marker) && Date.now() - started < 3000) {
await new Promise((resolve) => setTimeout(resolve, 20));
}
assert.equal(fs.existsSync(marker), true, `renderer did not start; stderr=${stderr}`);
const candidatePath = fs.readFileSync(marker, 'utf8');
const candidateRelative = path.relative(linkedDirectory, candidatePath);
fs.mkdirSync(path.dirname(path.join(inputDirectory, candidateRelative)), { recursive: true });
fs.unlinkSync(linkedDirectory);
fs.symlinkSync(inputDirectory, linkedDirectory, 'dir');
const status = await new Promise((resolve) => child.once('close', resolve));
assert.equal(status, 1, stderr);
const receipt = JSON.parse(stdout);
assert.equal(receipt.stage, 'commit');
assert.equal(receipt.diagnostics[0].code, 'output/input-alias');
assert.deepEqual(fs.readFileSync(input), source);
});
test('compare rejects an artifact path that aliases either architecture input', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-compare-'));
const realDirectory = path.join(cwd, 'real');
const linkedDirectory = path.join(cwd, 'linked');
fs.mkdirSync(realDirectory);
fs.symlinkSync(realDirectory, linkedDirectory, 'dir');
const base = path.join(realDirectory, 'review.html');
const output = path.join(linkedDirectory, 'review.html');
const baseSource = fs.readFileSync(baseFixture);
fs.writeFileSync(base, baseSource);
const result = run(['compare', 'architecture', base, headFixture, output, '--json'], cwd);
assert.equal(result.status, 1);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.stage, 'prepare');
assert.equal(receipt.diagnostics[0].code, 'output/input-alias');
assert.deepEqual(fs.readFileSync(base), baseSource);
});
test('compare rejects a receipt path that aliases either architecture input', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-compare-receipt-'));
const base = path.join(cwd, 'base.json');
const output = path.join(cwd, 'delta.html');
const baseSource = fs.readFileSync(baseFixture);
fs.writeFileSync(base, baseSource);
const result = run([
'compare', 'architecture', base, headFixture, output,
'--receipt', base, '--json',
], cwd);
assert.equal(result.status, 1);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.stage, 'prepare');
assert.equal(receipt.diagnostics[0].code, 'output/input-alias');
assert.deepEqual(fs.readFileSync(base), baseSource);
assert.equal(fs.existsSync(output), false);
});
test('compare rejects a dangling receipt symlink to the future artifact path', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-compare-pair-'));
const output = path.join(cwd, 'delta.html');
const receiptPath = path.join(cwd, 'delta.receipt.json');
fs.symlinkSync(output, receiptPath, 'file');
const result = run([
'compare', 'architecture', baseFixture, headFixture, output,
'--receipt', receiptPath, '--json',
], cwd);
assert.equal(result.status, 1);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.stage, 'prepare');
assert.equal(receipt.diagnostics[0].code, 'output/target-alias');
assert.equal(fs.lstatSync(receiptPath).isSymbolicLink(), true);
assert.equal(fs.existsSync(output), false);
});
test('preview applies the meta.output relative-path boundary before starting a server', async () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-preview-meta-'));
const input = path.join(cwd, 'diagram.workflow.json');
const output = path.join(cwd, 'authored.html');
const source = JSON.parse(fs.readFileSync(workflowFixture, 'utf8'));
source.meta.output = output;
fs.writeFileSync(input, JSON.stringify(source));
const failure = await startPreview({
type: 'workflow',
input,
open: false,
watch: false,
cwd,
}).then(async (preview) => {
await preview.stop();
return null;
}, (error) => error);
assert.ok(failure instanceof Error);
assert.match(failure.message, /meta\.output must be a relative path/i);
assert.equal(fs.existsSync(output), false);
});
test('the shared renderer rechecks its guarded output immediately before writing', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-render-race-'));
const inputDirectory = path.join(cwd, 'input');
const initialOutputDirectory = path.join(cwd, 'safe-output');
const linkedDirectory = path.join(cwd, 'linked-output');
fs.mkdirSync(inputDirectory);
fs.mkdirSync(initialOutputDirectory);
fs.symlinkSync(initialOutputDirectory, linkedDirectory, 'dir');
const input = path.join(inputDirectory, 'diagram.workflow.json');
const output = path.join(linkedDirectory, 'diagram.workflow.json');
const source = fs.readFileSync(workflowFixture);
fs.writeFileSync(input, source);
const loaded = loadDiagram({
rendererDir: path.join(skillRoot, 'renderers/workflow'),
diagramType: 'workflow',
defaultExample: 'agent-tool-call.workflow.json',
argv: ['node', 'render-workflow.mjs', input, output],
});
fs.unlinkSync(linkedDirectory);
fs.symlinkSync(inputDirectory, linkedDirectory, 'dir');
assert.throws(
() => writeDiagram({
outPath: loaded.outPath,
template: loaded.template,
diagramType: 'workflow',
meta: loaded.diagram.meta,
svg: '<svg role="img"></svg>',
cards: [],
}),
/output must not replace an input/i,
);
assert.deepEqual(fs.readFileSync(input), source);
});
test('compare rechecks every target immediately before committing the artifact pair', { timeout: 10000 }, async () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-compare-race-'));
const installedRoot = path.join(cwd, 'skill');
const installedBin = path.join(installedRoot, 'bin');
const installedShared = path.join(installedRoot, 'renderers/shared');
const installedRenderer = path.join(installedRoot, 'renderers/architecture');
const installedScripts = path.join(installedRoot, 'scripts');
const installedDelta = path.join(installedRoot, 'delta');
for (const directory of [installedBin, installedShared, installedRenderer, installedScripts, installedDelta]) {
fs.mkdirSync(directory, { recursive: true });
}
fs.copyFileSync(cli, path.join(installedBin, 'archify.mjs'));
fs.copyFileSync(
path.join(skillRoot, 'renderers/shared/output-path.mjs'),
path.join(installedShared, 'output-path.mjs'),
);
fs.writeFileSync(path.join(installedRenderer, 'render-architecture.mjs'), `
import fs from 'node:fs';
import path from 'node:path';
const [, output] = process.argv.slice(2);
if (path.basename(output) === 'head.html') {
const marker = process.env.ARCHIFY_TEST_RENDER_STARTED;
const markerCandidate = marker + '.tmp';
fs.writeFileSync(markerCandidate, output);
fs.renameSync(markerCandidate, marker);
await new Promise((resolve) => setTimeout(resolve, 500));
}
fs.writeFileSync(output, '<!doctype html><svg role="img"></svg>');
`);
fs.writeFileSync(path.join(installedScripts, 'check-render-output.mjs'), `
console.log(JSON.stringify({
ok: true,
checks: [{ name: 'single_svg', ok: true }],
composition: {
profile: 'showcase',
status: 'pass',
summary: { errors: 0, warnings: 0 }
}
}));
`);
fs.writeFileSync(path.join(installedDelta, 'architecture-delta.mjs'), `
export class ArchitectureDeltaError extends Error {}
export const annotateArchitectureSideSvg = (svg) => svg;
export const buildDeltaSvg = () => '<svg role="img"></svg>';
export const canonicalArchitecture = (value) => value;
export const canonicalArchitectureJson = (value) => JSON.stringify(value);
export const compareArchitecture = () => ({
command: 'compare',
base: {},
head: {},
completeness: 'complete',
proofLevel: 'authored'
});
export const extractArchitectureSvg = () => '<svg role="img"></svg>';
export const extractArtifactCss = () => '';
export const renderArchitectureDeltaHtml = () => '<!doctype html><svg role="img"></svg>';
export const validateArchitectureDeltaHtml = () => ({ checksPassed: 1, checkCount: 1 });
`);
const inputDirectory = path.join(cwd, 'input');
const initialOutputDirectory = path.join(cwd, 'safe-output');
const linkedDirectory = path.join(cwd, 'linked-output');
fs.mkdirSync(inputDirectory);
fs.mkdirSync(initialOutputDirectory);
fs.symlinkSync(initialOutputDirectory, linkedDirectory, 'dir');
const base = path.join(inputDirectory, 'diagram.json');
const head = path.join(cwd, 'head.json');
const output = path.join(linkedDirectory, 'diagram.json');
const source = Buffer.from('{"side":"base"}');
fs.writeFileSync(base, source);
fs.writeFileSync(head, '{"side":"head"}');
const marker = path.join(cwd, 'renderer-started');
const child = spawn(process.execPath, [
path.join(installedBin, 'archify.mjs'),
'compare', 'architecture', base, head, output, '--json',
], {
cwd,
encoding: 'utf8',
env: { ...process.env, ARCHIFY_TEST_RENDER_STARTED: marker },
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
const started = Date.now();
while (!fs.existsSync(marker) && Date.now() - started < 3000) {
await new Promise((resolve) => setTimeout(resolve, 20));
}
assert.equal(fs.existsSync(marker), true, `renderer did not start; stderr=${stderr}`);
const candidatePath = fs.readFileSync(marker, 'utf8');
const candidateRelative = path.relative(linkedDirectory, candidatePath);
fs.mkdirSync(path.dirname(path.join(inputDirectory, candidateRelative)), { recursive: true });
fs.unlinkSync(linkedDirectory);
fs.symlinkSync(inputDirectory, linkedDirectory, 'dir');
const status = await new Promise((resolve) => child.once('close', resolve));
assert.equal(status, 1, stderr);
const receipt = JSON.parse(stdout);
assert.equal(receipt.stage, 'commit');
assert.equal(receipt.diagnostics[0].code, 'output/input-alias');
assert.deepEqual(fs.readFileSync(base), source);
});
test('doctor reports a missing output-path safety runtime in an installed skill', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-output-doctor-'));
const installedRoot = path.join(cwd, 'skill');
copyInstalledSkill(installedRoot);
fs.rmSync(path.join(installedRoot, 'renderers/shared/output-path.mjs'));
const result = spawnSync(process.execPath, [path.join(installedRoot, 'bin/archify.mjs'), 'doctor'], {
cwd: installedRoot,
encoding: 'utf8',
});
assert.equal(result.status, 1);
assert.match(result.stdout, /\[missing\] Output path safety runtime/);
});
@@ -0,0 +1,71 @@
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-presentation-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function svg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all typed renderers ship the same presentation stage contract', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /id="btn-present"[^>]+aria-label="Enter presentation stage"[^>]+aria-pressed="false"/, mode);
assert.match(html, /Archify\.presentation = \(function \(\)/, mode);
assert.match(html, /enter: function \(\) \{ return setActive\(true\); \}/, mode);
assert.match(html, /exit: function \(\) \{ return setActive\(false\); \}/, mode);
assert.match(html, /html\[data-present="true"\]:not\(\[data-embed="true"\]\) \.diagram-container/, mode);
assert.match(html, /height: 100dvh/, mode);
assert.match(html, /\.cards \{ display: none; \}/, mode);
assert.doesNotMatch(html.match(/<html[^>]*>/)?.[0] || '', /data-present=/, mode);
assert.doesNotMatch(svg(html), /data-present|btn-present|Presentation Stage/, mode);
}
});
test('presentation stage supports direct links and preserves view hashes', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /get\('present'\) === '1'/);
assert.match(html, /document\.documentElement\.setAttribute\('data-present', 'true'\)/);
assert.match(html, /url\.searchParams\.set\('present', '1'\)/);
assert.match(html, /url\.searchParams\.delete\('present'\)/);
assert.match(html, /url\.pathname \+ url\.search \+ url\.hash/);
assert.match(html, /Embed mode wins when both query parameters are present/);
});
test('presentation keyboard behavior exits in layers and remains accessible', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /e\.defaultPrevented\) return/);
assert.match(html, /e\.key === 'f' \|\| e\.key === 'F'/);
assert.match(html, /Archify\.presentation\.toggle\(\)/);
assert.match(html, /e\.key === 'Escape' && Archify\.focus\.active\(\)/);
assert.match(html, /e\.key === 'Escape' && Archify\.presentation\.active\(\)/);
assert.match(html, /btn\.setAttribute\('aria-pressed', next \? 'true' : 'false'\)/);
assert.match(html, /Exit presentation stage \(F or Escape\)/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -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 }));
@@ -0,0 +1,40 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import assert from 'node:assert/strict';
const here = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(here, '..');
const repoRoot = path.resolve(skillRoot, '..');
const skill = fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8');
const delivery = fs.readFileSync(path.join(skillRoot, 'references', 'delivery-contract.md'), 'utf8');
const readme = fs.readFileSync(path.join(repoRoot, 'README.md'), 'utf8');
const english = fs.readFileSync(path.join(repoRoot, 'README_EN.md'), 'utf8');
const chinese = fs.readFileSync(path.join(repoRoot, 'README_ZH.md'), 'utf8');
test('preview contract: the skill keeps live preview explicit, desktop-only, and last-good', () => {
assert.match(delivery, /archify\.mjs preview <type> <input>\.json <output>\.html/);
assert.match(delivery, /active desktop authoring loop/i);
assert.match(delivery, /previous verified revision on screen and on disk/i);
assert.match(delivery, /never start it by default/i);
assert.match(delivery, /CI, unattended agents, remote sharing, or mobile use/i);
assert.match(delivery, /must never enter the generated artifact or any export/i);
});
test('preview contract: all README languages document the same optional command without changing the hero', () => {
assert.equal(readme, english);
for (const text of [readme, chinese]) {
assert.match(text, /bin\/archify\.mjs preview workflow/);
assert.match(text, /--no-open/);
assert.match(text, /127\.0\.0\.1/);
assert.match(text, /Ctrl-C/);
assert.match(text, /docs\/assets\/archify-readme-hero\.png/);
}
});
test('preview contract: the canonical delivery reference owns no-leak and zero-dependency boundaries', () => {
assert.match(delivery, /Last-Good Live Preview/);
assert.match(delivery, /zero-dependency Skill ZIP/i);
assert.match(delivery, /Server state, port, source path, diagnostics, error text, and reload tokens must never enter/i);
});
@@ -0,0 +1,499 @@
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import assert from 'node:assert/strict';
import vm from 'node:vm';
import { startPreview } from '../bin/preview.mjs';
const here = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(here, '..');
function sha256(file) {
return createHash('sha256').update(fs.readFileSync(file)).digest('hex');
}
async function stateAt(url) {
const response = await fetch(new URL('/state', url));
assert.equal(response.status, 200);
return response.json();
}
async function waitForState(url, predicate, message, timeoutMs = 12000) {
const started = Date.now();
let latest;
while (Date.now() - started < timeoutMs) {
latest = await stateAt(url);
if (predicate(latest)) return latest;
await new Promise((resolve) => setTimeout(resolve, 40));
}
assert.fail(`${message}; latest state: ${JSON.stringify(latest)}`);
}
function rawRequest(url, { method = 'GET', pathname = '/', hostHeader } = {}) {
const target = new URL(url);
return new Promise((resolve, reject) => {
const request = http.request({
hostname: target.hostname,
port: target.port,
method,
path: pathname,
headers: hostHeader ? { Host: hostHeader } : undefined,
}, (response) => {
let body = '';
response.setEncoding('utf8');
response.on('data', (chunk) => { body += chunk; });
response.on('end', () => resolve({ status: response.statusCode, body, headers: response.headers }));
});
request.on('error', reject);
request.end();
});
}
test('preview: rejects destructive or unsupported startup targets before watching', async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-preview-startup-'));
const input = path.join(tmp, 'diagram.json');
fs.writeFileSync(input, '{}');
await assert.rejects(
startPreview({ type: 'architecture', input, output: input, open: false }),
/must not replace its JSON input/i,
);
await assert.rejects(
startPreview({ type: 'mindmap', input, output: path.join(tmp, 'out.html'), open: false }),
/Unknown diagram type/i,
);
await assert.rejects(
startPreview({ type: 'architecture', input, output: path.join(tmp, 'out.html'), quality: 'pretty', open: false }),
/Unknown quality profile/i,
);
const realDirectory = path.join(tmp, 'real');
const linkedDirectory = path.join(tmp, 'linked');
fs.mkdirSync(realDirectory);
fs.symlinkSync(realDirectory, linkedDirectory, 'dir');
await assert.rejects(
startPreview({
type: 'architecture',
input: path.join(realDirectory, 'future.json'),
output: path.join(linkedDirectory, 'future.json'),
open: false,
}),
/must not replace its JSON input/i,
);
assert.deepEqual(fs.readdirSync(tmp).filter((name) => name.startsWith('.archify-preview-')), []);
});
test('preview: invalid candidates preserve the last verified artifact and repair automatically', { timeout: 30000 }, async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-preview-last-good-'));
const input = path.join(tmp, 'diagram.architecture.json');
const output = path.join(tmp, 'diagram.html');
const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples/web-app.architecture.json'), 'utf8'));
source.meta.title = 'Last Good One';
fs.writeFileSync(input, JSON.stringify(source));
const preview = await startPreview({
type: 'architecture',
input,
output,
quality: 'showcase',
open: false,
debounceMs: 60,
pollMs: 80,
});
try {
const first = await waitForState(preview.url, (state) => state.status === 'verified' && state.revision === 1, 'first revision did not verify');
assert.equal(first.generation, 1);
assert.equal(first.lastVerified.sha256, sha256(output));
const firstSha = sha256(output);
const firstArtifact = await (await fetch(new URL('/artifact.html', preview.url))).text();
assert.match(firstArtifact, /Last Good One/);
fs.rmSync(input);
const missing = await waitForState(preview.url, (state) => state.status === 'needs-fix' && state.generation === 2, 'deleted source did not report failure');
assert.equal(missing.failure.stage, 'input');
assert.equal(missing.revision, 1);
assert.equal(sha256(output), firstSha, 'deleted input replaced the last verified output');
fs.writeFileSync(input, '{"meta":');
const failed = await waitForState(preview.url, (state) => state.status === 'needs-fix' && state.generation === 3, 'invalid source did not report failure');
assert.equal(failed.revision, 1);
assert.equal(failed.failure.stage, 'input');
assert.match(failed.failure.message, /Could not read delivery input/);
assert.doesNotMatch(JSON.stringify(failed), new RegExp(input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
assert.equal(sha256(output), firstSha, 'invalid input replaced the last verified output');
assert.equal(await (await fetch(new URL('/artifact.html', preview.url))).text(), firstArtifact);
source.components[0].unexpected = true;
fs.writeFileSync(input, JSON.stringify(source));
const schemaFailed = await waitForState(preview.url, (state) => state.status === 'needs-fix' && state.generation === 4, 'schema failure did not report render stage');
assert.equal(schemaFailed.failure.stage, 'render');
assert.match(schemaFailed.failure.message, /\/components\/0.*additional properties/i);
assert.doesNotMatch(schemaFailed.failure.message, /file:\/\/|\/Users\/|node:internal/);
assert.equal(sha256(output), firstSha, 'schema failure replaced the last verified output');
delete source.components[0].unexpected;
source.meta.title = 'Verified Repair';
source.components[0].label = 'Repaired Browser';
fs.writeFileSync(input, JSON.stringify(source));
const repaired = await waitForState(preview.url, (state) => state.status === 'verified' && state.revision === 2, 'repaired source did not publish');
assert.equal(repaired.generation, 5);
assert.notEqual(repaired.lastVerified.sha256, firstSha);
const repairedArtifact = await (await fetch(new URL('/artifact.html', preview.url))).text();
assert.match(repairedArtifact, /Verified Repair/);
assert.match(repairedArtifact, /Repaired Browser/);
assert.equal(repaired.lastVerified.sha256, sha256(output));
const page = await rawRequest(preview.url);
assert.equal(page.status, 200);
assert.match(page.body, /Archify Live Preview/);
assert.match(page.body, /<summary role="button" aria-controls="diagnostic-panel">View diagnostic<\/summary>/);
assert.match(page.headers['content-security-policy'], /default-src 'none'/);
const script = page.body.match(/<script>\n([\s\S]*?)\n <\/script>/)?.[1];
assert.ok(script, 'preview shell script missing');
assert.doesNotThrow(() => new vm.Script(script));
assert.equal((await rawRequest(preview.url, { method: 'POST' })).status, 405);
assert.equal((await rawRequest(preview.url, { pathname: '/../../etc/passwd' })).status, 404);
assert.equal((await rawRequest(preview.url, { hostHeader: 'example.com' })).status, 403);
} finally {
await preview.stop();
}
await assert.rejects(fetch(preview.url));
assert.deepEqual(fs.readdirSync(tmp).filter((name) => name.startsWith('.archify-preview-')), []);
});
test('preview: content digests suppress identical writes and a burst publishes only its stable tail', { timeout: 30000 }, async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-preview-burst-'));
const input = path.join(tmp, 'diagram.workflow.json');
const output = path.join(tmp, 'diagram.html');
const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples/agent-tool-call.workflow.json'), 'utf8'));
const original = JSON.stringify(source);
fs.writeFileSync(input, original);
const preview = await startPreview({
type: 'workflow',
input,
output,
open: false,
debounceMs: 90,
pollMs: 70,
});
try {
await waitForState(preview.url, (state) => state.status === 'verified' && state.revision === 1, 'initial workflow did not verify');
fs.writeFileSync(input, original);
await new Promise((resolve) => setTimeout(resolve, 350));
let state = await stateAt(preview.url);
assert.equal(state.generation, 1);
assert.equal(state.revision, 1);
fs.writeFileSync(input, JSON.stringify(source, null, 2));
state = await waitForState(preview.url, (candidate) => candidate.status === 'verified' && candidate.generation === 2, 'semantically identical source did not settle');
assert.equal(state.revision, 1, 'identical artifact bytes triggered a browser revision');
for (let index = 0; index < 8; index += 1) {
source.meta.title = `Burst ${index}`;
fs.writeFileSync(input, JSON.stringify(source));
await new Promise((resolve) => setTimeout(resolve, 12));
}
source.meta.title = 'Stable Tail';
fs.writeFileSync(input, JSON.stringify(source));
state = await waitForState(preview.url, (candidate) => candidate.status === 'verified' && candidate.revision === 2, 'stable burst tail did not verify');
assert.equal(state.generation, 3);
const artifact = await (await fetch(new URL('/artifact.html', preview.url))).text();
assert.match(artifact, /Stable Tail/);
assert.doesNotMatch(artifact, /Burst 7/);
} finally {
await preview.stop();
}
});
test('preview: a superseded slow candidate can never become a published revision', { timeout: 30000 }, async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-preview-latest-wins-'));
const input = path.join(tmp, 'diagram.json');
const output = path.join(tmp, 'diagram.html');
const deliveryCli = path.join(tmp, 'fake-delivery.mjs');
fs.writeFileSync(deliveryCli, `
import { createHash } from 'node:crypto';
import fs from 'node:fs';
const [, , input, output] = process.argv.slice(2);
const source = JSON.parse(fs.readFileSync(input, 'utf8'));
await new Promise((resolve) => setTimeout(resolve, source.title === 'Slow Old' ? 550 : 40));
const artifact = Buffer.from('<!doctype html><title>' + source.title + '</title><svg></svg>');
fs.writeFileSync(output, artifact);
console.log(JSON.stringify({
ok: true,
artifact: { sha256: createHash('sha256').update(artifact).digest('hex'), bytes: artifact.byteLength },
validation: { checksPassed: 1, checkCount: 1, compositionProfile: 'showcase', compositionStatus: 'pass' }
}));
`);
fs.writeFileSync(input, JSON.stringify({ title: 'Slow Old' }));
const preview = await startPreview({
type: 'architecture',
input,
output,
open: false,
debounceMs: 25,
pollMs: 40,
deliveryCli,
});
try {
await waitForState(preview.url, (state) => state.status === 'checking' && state.generation === 1, 'slow generation did not start');
await new Promise((resolve) => setTimeout(resolve, 100));
fs.writeFileSync(input, JSON.stringify({ title: 'Fast New' }));
const state = await waitForState(preview.url, (candidate) => candidate.status === 'verified' && candidate.generation === 2, 'latest generation did not publish');
assert.equal(state.revision, 1, 'superseded generation was published before the latest one');
const artifact = await (await fetch(new URL('/artifact.html', preview.url))).text();
assert.match(artifact, /Fast New/);
assert.doesNotMatch(artifact, /Slow Old/);
assert.equal(fs.readFileSync(output, 'utf8'), artifact);
} finally {
await preview.stop();
}
});
test('preview: each delivery reads the immutable bytes bound to its observed digest', { timeout: 30000 }, async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-preview-snapshot-'));
const input = path.join(tmp, 'diagram.json');
const output = path.join(tmp, 'diagram.html');
const deliveryCli = path.join(tmp, 'snapshot-delivery.mjs');
const readMarker = path.join(tmp, 'delivery-read.txt');
fs.writeFileSync(deliveryCli, `
import { createHash } from 'node:crypto';
import fs from 'node:fs';
const [, , input, output] = process.argv.slice(2);
await new Promise((resolve) => setTimeout(resolve, 120));
const source = JSON.parse(fs.readFileSync(input, 'utf8'));
fs.writeFileSync(${JSON.stringify(readMarker)}, source.title);
await new Promise((resolve) => setTimeout(resolve, 180));
const artifact = Buffer.from('<!doctype html><title>' + source.title + '</title><svg></svg>');
fs.writeFileSync(output, artifact);
console.log(JSON.stringify({
ok: true,
artifact: { sha256: createHash('sha256').update(artifact).digest('hex'), bytes: artifact.byteLength },
validation: { checksPassed: 1, checkCount: 1, compositionProfile: 'showcase', compositionStatus: 'pass' }
}));
`);
fs.writeFileSync(input, JSON.stringify({ title: 'Source A' }));
const preview = await startPreview({
type: 'architecture',
input,
output,
open: false,
debounceMs: 10,
pollMs: 5000,
watch: false,
deliveryCli,
});
try {
await waitForState(preview.url, (state) => state.status === 'checking' && state.generation === 1, 'snapshot generation did not start');
fs.writeFileSync(input, JSON.stringify({ title: 'Source B' }));
const markerStarted = Date.now();
while (!fs.existsSync(readMarker) && Date.now() - markerStarted < 3000) {
await new Promise((resolve) => setTimeout(resolve, 20));
}
assert.ok(fs.existsSync(readMarker), 'fake delivery never read its generation input');
fs.writeFileSync(input, JSON.stringify({ title: 'Source A' }));
const state = await waitForState(preview.url, (candidate) => candidate.status === 'verified' && candidate.revision === 1, 'snapshot generation did not verify');
assert.equal(state.generation, 1, 'an unobserved A → B → A edit started a second generation');
assert.equal(fs.readFileSync(readMarker, 'utf8'), 'Source A');
assert.match(fs.readFileSync(output, 'utf8'), /Source A/);
assert.doesNotMatch(fs.readFileSync(output, 'utf8'), /Source B/);
} finally {
await preview.stop();
}
});
test('preview: commit rechecks the live digest when watcher and poll have not seen a newer save', { timeout: 10000 }, async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-preview-commit-race-'));
const input = path.join(tmp, 'diagram.json');
const output = path.join(tmp, 'diagram.html');
const deliveryCli = path.join(tmp, 'commit-race-delivery.mjs');
fs.writeFileSync(deliveryCli, `
import { createHash } from 'node:crypto';
import fs from 'node:fs';
const [, , input, output] = process.argv.slice(2);
const source = JSON.parse(fs.readFileSync(input, 'utf8'));
await new Promise((resolve) => setTimeout(resolve, source.title === 'Prior Good' ? 30 : 260));
const artifact = Buffer.from('<!doctype html><title>' + source.title + '</title><svg></svg>');
fs.writeFileSync(output, artifact);
console.log(JSON.stringify({
ok: true,
artifact: { sha256: createHash('sha256').update(artifact).digest('hex'), bytes: artifact.byteLength },
validation: { checksPassed: 1, checkCount: 1, compositionProfile: 'showcase', compositionStatus: 'pass' }
}));
`);
fs.writeFileSync(input, JSON.stringify({ title: 'Prior Good' }));
const preview = await startPreview({
type: 'architecture',
input,
output,
open: false,
debounceMs: 10,
pollMs: 800,
watch: false,
deliveryCli,
});
try {
await waitForState(preview.url, (state) => state.status === 'verified' && state.revision === 1, 'prior good revision did not verify');
const priorArtifact = fs.readFileSync(output, 'utf8');
fs.writeFileSync(input, JSON.stringify({ title: 'Intermediate A' }));
await waitForState(preview.url, (state) => state.status === 'checking' && state.generation === 2, 'intermediate generation did not start');
fs.writeFileSync(input, JSON.stringify({ title: 'Current B' }));
await new Promise((resolve) => setTimeout(resolve, 340));
assert.equal(fs.readFileSync(output, 'utf8'), priorArtifact, 'superseded intermediate bytes replaced the prior last-good output');
const pending = await stateAt(preview.url);
assert.equal(pending.revision, 1, 'superseded intermediate bytes advanced the browser revision');
const current = await waitForState(preview.url, (state) => state.status === 'verified' && state.generation === 3, 'current generation did not verify');
assert.equal(current.revision, 2);
assert.match(fs.readFileSync(output, 'utf8'), /Current B/);
assert.doesNotMatch(fs.readFileSync(output, 'utf8'), /Intermediate A/);
} finally {
await preview.stop();
}
});
test('preview: stopping drains an active delivery without publishing it', { timeout: 30000 }, async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-preview-stop-'));
const input = path.join(tmp, 'diagram.json');
const output = path.join(tmp, 'diagram.html');
const deliveryCli = path.join(tmp, 'slow-delivery.mjs');
const prior = '<!doctype html><title>Prior verified artifact</title>';
fs.writeFileSync(output, prior);
fs.writeFileSync(input, JSON.stringify({ title: 'Do not publish after stop' }));
fs.writeFileSync(deliveryCli, `
import { createHash } from 'node:crypto';
import fs from 'node:fs';
const [, , , output] = process.argv.slice(2);
await new Promise((resolve) => setTimeout(resolve, 450));
const artifact = Buffer.from('<!doctype html><title>Late candidate</title><svg></svg>');
fs.writeFileSync(output, artifact);
console.log(JSON.stringify({
ok: true,
artifact: { sha256: createHash('sha256').update(artifact).digest('hex'), bytes: artifact.byteLength },
validation: { checksPassed: 1, checkCount: 1, compositionProfile: 'showcase', compositionStatus: 'pass' }
}));
`);
const preview = await startPreview({
type: 'architecture',
input,
output,
open: false,
debounceMs: 10,
pollMs: 100,
deliveryCli,
});
await waitForState(preview.url, (state) => state.status === 'checking' && state.generation === 1, 'slow stop candidate did not start');
await new Promise((resolve) => setTimeout(resolve, 90));
const stoppedAt = Date.now();
await preview.stop();
assert.ok(Date.now() - stoppedAt >= 250, 'preview did not drain the active delivery');
assert.equal(fs.readFileSync(output, 'utf8'), prior);
assert.deepEqual(fs.readdirSync(tmp).filter((name) => name.startsWith('.archify-preview-')), []);
});
test('preview: stopping has a bounded kill path for a delivery that never exits', { timeout: 5000 }, async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-preview-hung-stop-'));
const input = path.join(tmp, 'diagram.json');
const output = path.join(tmp, 'diagram.html');
const deliveryCli = path.join(tmp, 'hung-delivery.mjs');
const prior = '<!doctype html><title>Keep me</title>';
fs.writeFileSync(input, JSON.stringify({ title: 'Never completes' }));
fs.writeFileSync(output, prior);
fs.writeFileSync(deliveryCli, `
process.on('SIGTERM', () => {});
setInterval(() => {}, 1000);
`);
const preview = await startPreview({
type: 'architecture',
input,
output,
open: false,
debounceMs: 10,
pollMs: 5000,
deliveryCli,
stopGraceMs: 80,
stopKillMs: 80,
});
await waitForState(preview.url, (state) => state.status === 'checking' && state.generation === 1, 'hung generation did not start');
await new Promise((resolve) => setTimeout(resolve, 80));
const stoppedAt = Date.now();
await preview.stop();
assert.ok(Date.now() - stoppedAt < 1000, 'hung delivery kept preview shutdown open');
assert.equal(fs.readFileSync(output, 'utf8'), prior);
await assert.rejects(fetch(preview.url));
assert.deepEqual(fs.readdirSync(tmp).filter((name) => name.startsWith('.archify-preview-')), []);
});
test('preview: checker failures keep their actionable detail instead of a generic stage only', { timeout: 30000 }, async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-preview-checker-'));
const input = path.join(tmp, 'diagram.json');
const output = path.join(tmp, 'diagram.html');
const deliveryCli = path.join(tmp, 'checker-failure.mjs');
fs.writeFileSync(input, '{}');
fs.writeFileSync(deliveryCli, `
console.log(JSON.stringify({
ok: false,
stage: 'check',
error: 'Final artifact check failed; the previous artifact was preserved.',
checker: { checks: [{ name: 'single_svg', ok: false, details: ['found 2 <svg> blocks; expected exactly one'] }] }
}));
process.exitCode = 1;
`);
const preview = await startPreview({
type: 'architecture',
input,
output,
open: false,
debounceMs: 10,
pollMs: 100,
deliveryCli,
});
try {
const state = await waitForState(preview.url, (candidate) => candidate.status === 'needs-fix', 'checker failure did not surface');
assert.equal(state.failure.stage, 'check');
assert.match(state.failure.message, /Final artifact check failed/);
assert.match(state.failure.message, /found 2 <svg> blocks; expected exactly one/);
} finally {
await preview.stop();
}
});
test('preview: all five typed renderers reach a verified first revision', { timeout: 60000 }, async () => {
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',
};
for (const [type, example] of Object.entries(cases)) {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `archify-preview-${type}-`));
const input = path.join(tmp, example);
const output = path.join(tmp, `${type}.html`);
fs.copyFileSync(path.join(skillRoot, 'examples', example), input);
const preview = await startPreview({ type, input, output, open: false, debounceMs: 10, pollMs: 500 });
try {
const state = await waitForState(preview.url, (candidate) => candidate.status === 'verified', `${type} did not verify`);
assert.equal(state.revision, 1, type);
assert.equal(state.lastVerified.checksPassed, state.lastVerified.checkCount, type);
assert.equal(state.lastVerified.sha256, sha256(output), type);
} finally {
await preview.stop();
}
}
});
@@ -0,0 +1,78 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const landing = fs.readFileSync(path.resolve(__dirname, '..', '..', 'docs', 'index.html'), 'utf8');
function cssRule(selector) {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = landing.match(new RegExp(`${escaped}\\s*\\{([^}]+)\\}`));
assert.ok(match, `${selector}: CSS rule missing`);
return match[1];
}
test('landing declares a truthful first-fold proof aperture in document order', () => {
assert.match(landing, /<section class="hero" data-proof-aperture="first-fold">/);
const heroStart = landing.indexOf('data-proof-aperture="first-fold"');
const headline = landing.indexOf('data-i18n="hero-h1"', heroStart);
const actions = landing.indexOf('class="hero-actions', headline);
const proof = landing.indexOf('id="hero-proof-stage"', actions);
assert.ok(heroStart < headline && headline < actions && actions < proof);
});
test('desktop hero budget exposes live diagram content without shrinking its canvas', () => {
assert.match(cssRule('.hero'), /padding-top:9rem/);
assert.match(cssRule('.hero-bento'), /grid-template-columns:repeat\(12,1fr\)/);
assert.match(cssRule('.hero-intro'), /grid-column:1 \/ 8/);
assert.match(cssRule('.proof-main'), /grid-column:8 \/ 13/);
assert.match(cssRule('.proof-main'), /grid-row:1 \/ 3/);
assert.match(cssRule('.hero-actions .btn'), /min-height:44px/);
assert.match(cssRule('.proof-viewport'), /min-height:430px/);
});
test('narrow viewport preserves a contained fallback without adding a mobile product surface', () => {
const mobile = landing.match(/@media\(max-width:640px\)\s*\{([\s\S]+?)\n\s*\}\n\s*<\/style>/)?.[1];
assert.ok(mobile, 'narrow mobile media query missing');
assert.match(mobile, /\.hero\s*\{\s*padding-top:6\.75rem;\s*\}/);
assert.match(mobile, /\.hero-actions \.btn\s*\{\s*flex:1;\s*justify-content:center;\s*\}/);
assert.match(mobile, /\.proof-viewport\s*\{\s*min-height:360px;\s*\}/);
assert.match(mobile, /\.proof-rail\s*\{\s*grid-template-columns:1fr;\s*\}/);
});
test('proof aperture remains one real eager sandboxed artifact with explicit user-selected identities', () => {
assert.equal((landing.match(/<iframe id="hero-proof-frame"/g) || []).length, 1);
assert.match(landing, /loading="eager"/);
assert.match(landing, /sandbox="allow-scripts"/);
assert.doesNotMatch(landing, /sandbox="[^"]*allow-same-origin/);
assert.equal((landing.match(/class="spec-card"/g) || []).length, 3);
assert.match(landing, /data-proof-playback="first-fold-once"/);
assert.match(landing, /\?embed=1&amp;play=1&amp;theme=dark#view=happy-path/);
assert.doesNotMatch(landing, /setInterval\(|scrollIntoView\(|scroll-triggered|proof-carousel/);
});
test('initial proof playback uses one sandboxed load without parent-frame reach-through', () => {
assert.match(landing, /src="gallery\/artifacts\/agent-tool-call\.workflow\.html\?embed=1&amp;play=1&amp;theme=dark#view=happy-path"/);
assert.doesNotMatch(landing, /initialProof|proofFrameDocumentIsReady|proofFrame\.contentWindow|proofFrame\.contentDocument/);
assert.match(landing, /proofFrame\.addEventListener\('load', \(\) => \{/);
assert.match(landing, /proofStage\.classList\.remove\('is-loading'\)/);
});
test('proof playback delegates reduced motion to the artifact and keeps deliberate-choice boundaries', () => {
assert.match(landing, /renderProof\(tab\.dataset\.proof, \{ deliberate: true \}\)/);
assert.match(landing, /renderProof\(tabs\[next\]\.dataset\.proof, \{ focus: true, deliberate: true \}\)/);
assert.match(landing, /proofEmbedUrl\(proof, \{ play: deliberate \}\)/);
assert.match(landing, /document\.querySelectorAll\('\.fade-up'\)\.forEach\(el => el\.classList\.add\('visible'\)\)/);
assert.doesNotMatch(landing, /addEventListener\('scroll'/);
});
test('aperture uses normal flow and preserves reduced-motion boundaries', () => {
const hero = cssRule('.hero');
const proof = cssRule('.proof-main');
assert.doesNotMatch(hero + proof, /position:absolute|transform:|top:-|margin-top:-|height:100vh/);
assert.match(landing, /@media\s*\(prefers-reduced-motion:\s*reduce\)/);
assert.match(landing, /\.fade-up\s*\{\s*opacity:1!important;\s*transform:none!important;/);
assert.match(landing, /\.pulse-dot,\.proof-live::before\s*\{\s*animation:none!important;\s*\}/);
});
@@ -0,0 +1,149 @@
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 repoRoot = path.resolve(skillRoot, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-reach-share-card-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all five renderers inherit one active-reach-only Reach Share Card item', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /data-action="reach-share-card"[^>]*hidden disabled[^>]*>[\s\S]*?Reach Share Card[\s\S]*?1200(?:&times;|×)630 PNG/, mode);
assert.match(html, /function syncReachShareItem\(\)/, mode);
assert.match(html, /reachShareItem\.hidden = !snapshot;/, mode);
assert.match(html, /reachShareItem\.disabled = !snapshot;/, mode);
assert.match(html, /function open\(focusLast\)[\s\S]*?syncReachShareItem\(\);/, mode);
assert.doesNotMatch(html, /id="reach-share-card"|class="reach-share-card"/, mode);
assert.doesNotMatch(canonicalSvg(html), /data-share-reach(?:-|=)/, mode);
}
});
test('Reach Share Card snapshots copy the active authored closure without rerunning traversal', () => {
const html = render('architecture', CASES.architecture);
const start = html.indexOf('function reachabilitySnapshot() {');
const end = html.indexOf('\n function setPassportValue', start);
const snapshotBlock = start >= 0 && end > start ? html.slice(start, end) : '';
assert.match(snapshotBlock, /activeReachability\.nodeIds\.slice\(\)/);
assert.match(snapshotBlock, /activeReachability\.edgeKeys\.slice\(\)/);
assert.match(snapshotBlock, /activeReachability\.depths/);
assert.match(snapshotBlock, /direction: reachabilityMode/);
assert.match(snapshotBlock, /origin: \{ id: originId, label:/);
assert.match(snapshotBlock, /maxDepth: activeReachability\.maxDepth/);
assert.match(snapshotBlock, /seenNodeIds = Object\.create\(null\)/);
assert.match(snapshotBlock, /seenEdgeKeys = Object\.create\(null\)/);
assert.match(snapshotBlock, /drawableFragments = fragments\.filter\(hasDrawableGeometry\)/);
assert.match(snapshotBlock, /drawableFragments\.length !== 1/);
assert.match(snapshotBlock, /return null/);
assert.doesNotMatch(snapshotBlock, /computeReachability|reachabilityFor|queue\s*=|shortestDirectedPath/);
assert.match(html, /reachabilitySnapshot: reachabilitySnapshot/);
});
test('Reach variant decorates only a finite canonical clone with static authored identity', () => {
const html = render('workflow', CASES.workflow);
const start = html.indexOf('function applyReachSnapshot(clone, snapshot) {');
const end = html.indexOf('\n function serializeSvg', start);
const applyBlock = start >= 0 && end > start ? html.slice(start, end) : '';
assert.match(applyBlock, /snapshot\.direction !== 'upstream'/);
assert.match(applyBlock, /snapshot\.direction !== 'downstream'/);
assert.match(applyBlock, /snapshot\.nodeIds\.length < 2/);
assert.match(applyBlock, /snapshot\.origin\.label\.trim\(\)/);
assert.match(applyBlock, /nodeId === snapshot\.origin\.id \? depth !== 0 : depth < 1/);
assert.match(applyBlock, /edge\.depth !== Math\.max\(snapshot\.depths\[edge\.from\], snapshot\.depths\[edge\.to\]\)/);
assert.match(applyBlock, /matchedNodes\.length !== 1/);
assert.match(applyBlock, /drawableMatches\.length !== 1/);
assert.match(applyBlock, /data-share-reach-match/);
assert.match(applyBlock, /data-share-reach-origin/);
assert.match(applyBlock, /data-share-reach-depth/);
assert.match(applyBlock, /clone\.setAttribute\('data-share-reach', snapshot\.direction\)/);
assert.doesNotMatch(applyBlock, /setAttribute\('data-reach-(?:active|match|origin|depth)/);
assert.doesNotMatch(applyBlock, /animation:|setTimeout|requestAnimationFrame/);
assert.match(html, /canonicalStateClean && finiteSvgDimensions && !opts\.routeSnapshot && applyReachSnapshot\(clone, opts\.reachSnapshot\)/);
assert.ok(html.indexOf('var canonicalStateClean =') < html.indexOf('applyReachSnapshot(clone, opts.reachSnapshot)'), 'canonical cleanup must precede reach decoration');
});
test('Reach styling preserves context, direction, and Blueprint restraint without motion', () => {
const html = render('dataflow', CASES.dataflow);
assert.match(html, /svg\[data-share-reach\] \[data-node-id\], svg\[data-share-reach\] \[data-edge-from\] \{ opacity: 0\.14; \}/);
assert.match(html, /svg\[data-share-reach\] \[data-share-reach-match\] \{ opacity: 1; \}/);
assert.match(html, /data-share-reach=\\?"upstream\\?"[\s\S]*?--database-stroke/);
assert.match(html, /data-share-reach=\\?"downstream\\?"[\s\S]*?--backend-stroke/);
assert.match(html, /data-preset=\\?"blueprint\\?"\]\[data-share-reach\][\s\S]*?filter: none/);
const reachStyleBlock = html.match(/if \(opts\.reachSnapshot\) \{[\s\S]*?\n \}/)?.[0] || '';
assert.doesNotMatch(reachStyleBlock, /animation:|display:\s*none|transform:/);
});
test('Reach Share Card reuses the 1200x630 seam and publishes a truthful scoped receipt', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /options\.variant !== 'route' && options\.variant !== 'reach'/);
assert.match(html, /Archify\.focus\.reachabilitySnapshot\(\)/);
assert.match(html, /renderShareCard\(\{ reachSnapshot: snapshot \}\)/);
assert.doesNotMatch(html, /function rasterizeReachShareCard|reachShareCard:/);
assert.match(html, /viewerText\('viewer\.export\.card\.reachSummary'/);
assert.match(html, /direction: directionLabel/);
assert.match(html, /origin: reachSnapshot\.origin\.label/);
assert.match(html, /reachSnapshot\.nodeIds\.length - 1/);
assert.match(html, /reachSnapshot\.edges\.length/);
assert.match(html, /reachSnapshot\.maxDepth/);
assert.match(html, /recordExportReceipt\('share-card', blob, false, \{ width: SHARE_CARD_WIDTH, height: SHARE_CARD_HEIGHT \}, 'reach', false, true\)/);
assert.match(html, /'-' \+ snapshot\.direction \+ '-reach-share-card\.png'/);
assert.match(html, /data-last-export-reach-state-clean/);
assert.match(html, /Trace authored reach before exporting a Reach Share Card/);
assert.match(html, /downloadReachShareCard: runReachShareCard/);
});
test('Skill, product docs, and READMEs keep the optional truthful boundary explicit', () => {
const viewer = fs.readFileSync(path.join(skillRoot, 'references', 'viewer-runtime.md'), 'utf8');
assert.match(viewer, /Export → Reach Share Card/);
assert.match(viewer, /variant=reach/);
assert.match(viewer, /data-share-reach-\*/);
assert.match(viewer, /authored reachability/i);
assert.match(viewer, /download-only/i);
for (const readme of ['README.md', 'README_EN.md', 'README_ZH.md']) {
const text = fs.readFileSync(path.join(repoRoot, readme), 'utf8');
assert.match(text, /Reach Share Card/, readme);
assert.match(text, /docs\/assets\/mco-runtime-reach-share-card\.png/, readme);
}
const png = fs.readFileSync(path.join(repoRoot, 'docs/assets/mco-runtime-reach-share-card.png'));
assert.equal(png.subarray(0, 8).toString('hex'), '89504e470d0a1a0a');
assert.equal(png.readUInt32BE(16), 1200);
assert.equal(png.readUInt32BE(20), 630);
const product = fs.readFileSync(path.join(repoRoot, 'PRODUCT.md'), 'utf8');
const design = fs.readFileSync(path.join(repoRoot, 'DESIGN.md'), 'utf8');
assert.match(product, /Reach Share Card/);
assert.match(design, /Reach Share Card/);
assert.match(design, /not (?:runtime )?(?:impact|causality|breakage)/i);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,295 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import crypto from 'node:crypto';
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 repoRoot = path.resolve(skillRoot, '..');
const assetPath = path.join(repoRoot, 'docs', 'assets', 'archify-live-proof.gif');
const receiptPath = path.join(repoRoot, 'docs', 'assets', 'archify-live-proof.json');
function sha256(file) {
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
}
function git(cwd, ...args) {
return execFileSync('git', args, { cwd, encoding: 'utf8' }).trim();
}
function writeStarHistoryCharts(cwd, version) {
const assets = path.join(cwd, 'assets');
fs.mkdirSync(assets, { recursive: true });
fs.writeFileSync(path.join(assets, 'star-history-light.svg'), `<svg><title>light ${version}</title></svg>\n`);
fs.writeFileSync(path.join(assets, 'star-history-dark.svg'), `<svg><title>dark ${version}</title></svg>\n`);
}
function skipSubBlocks(buffer, start) {
let offset = start;
while (offset < buffer.length) {
const size = buffer[offset];
offset += 1;
if (size === 0) return offset;
offset += size;
}
throw new Error('GIF sub-block runs past end of file');
}
function inspectGif(buffer) {
assert.match(buffer.subarray(0, 6).toString('ascii'), /^GIF8[79]a$/);
const width = buffer.readUInt16LE(6);
const height = buffer.readUInt16LE(8);
const packed = buffer[10];
let offset = 13;
if (packed & 0x80) offset += 3 * (2 ** ((packed & 0x07) + 1));
let frameCount = 0;
let durationCentiseconds = 0;
let trailer = false;
while (offset < buffer.length) {
const marker = buffer[offset];
offset += 1;
if (marker === 0x3b) {
trailer = true;
break;
}
if (marker === 0x21) {
const label = buffer[offset];
offset += 1;
if (label === 0xf9) {
const blockSize = buffer[offset];
offset += 1;
assert.equal(blockSize, 4, 'unexpected graphic-control block size');
durationCentiseconds += buffer.readUInt16LE(offset + 1);
offset += blockSize;
assert.equal(buffer[offset], 0, 'graphic-control block missing terminator');
offset += 1;
} else {
offset = skipSubBlocks(buffer, offset);
}
continue;
}
if (marker === 0x2c) {
frameCount += 1;
const localPacked = buffer[offset + 8];
offset += 9;
if (localPacked & 0x80) offset += 3 * (2 ** ((localPacked & 0x07) + 1));
offset += 1;
offset = skipSubBlocks(buffer, offset);
continue;
}
throw new Error(`unexpected GIF marker 0x${marker.toString(16)} at ${offset - 1}`);
}
assert.equal(trailer, true, 'GIF trailer missing');
return { width, height, frameCount, durationSeconds: durationCentiseconds / 100 };
}
test('README motion proof is compact, looping, and backed by current gallery artifacts', () => {
const builder = fs.readFileSync(path.join(repoRoot, 'scripts', 'build-readme-showcase.mjs'), 'utf8');
assert.match(builder, /\?embed=1&play=1&theme=dark#view=/);
const buffer = fs.readFileSync(assetPath);
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
const inspected = inspectGif(buffer);
assert.deepEqual(inspected, { width: 960, height: 540, frameCount: 54, durationSeconds: 5.4 });
assert.ok(buffer.includes(Buffer.from('NETSCAPE2.0')), 'GIF must loop continuously');
assert.ok(buffer.byteLength <= 3 * 1024 * 1024, `README GIF is too large: ${buffer.byteLength} bytes`);
assert.equal(receipt.schemaVersion, 1);
assert.equal(receipt.generator, 'scripts/build-readme-showcase.mjs');
assert.equal(receipt.output, 'docs/assets/archify-live-proof.gif');
assert.equal(receipt.width, inspected.width);
assert.equal(receipt.height, inspected.height);
assert.equal(receipt.frameCount, inspected.frameCount);
assert.equal(receipt.durationSeconds, inspected.durationSeconds);
assert.equal(receipt.bytes, buffer.byteLength);
assert.equal(receipt.sha256, sha256(assetPath));
assert.deepEqual(receipt.scenes.map(scene => scene.id), ['signal-flow', 'blueprint', 'classic']);
for (const scene of receipt.scenes) {
const artifact = path.join(repoRoot, scene.artifact);
assert.ok(fs.existsSync(artifact), `${scene.id}: source artifact missing`);
assert.equal(scene.artifactSha256, sha256(artifact), `${scene.id}: source artifact drift; rebuild README showcase`);
assert.match(scene.receipt, /9\/9 checks/);
}
});
test('all README languages keep the product hero and retain the verified animated proof', () => {
for (const filename of ['README.md', 'README_EN.md', 'README_ZH.md']) {
const readme = fs.readFileSync(path.join(repoRoot, filename), 'utf8');
const heroIndex = readme.indexOf('docs/assets/archify-readme-hero.png');
const titleIndex = readme.indexOf('# Archify');
const proofIndex = readme.indexOf('docs/assets/archify-live-proof.gif');
const demosIndex = Math.max(readme.indexOf('## See Archify in action'), readme.indexOf('## 看看 Archify 能做什么'));
assert.ok(heroIndex >= 0 && heroIndex < titleIndex, `${filename}: product hero is not above the title`);
assert.ok(proofIndex > demosIndex, `${filename}: animated proof must live in the demo section`);
assert.match(readme, /docs\/assets\/archify-live-proof\.gif/);
assert.match(readme, /https:\/\/tt-a1i\.github\.io\/archify\/gallery\.html/);
}
assert.equal(
fs.readFileSync(path.join(repoRoot, 'README.md'), 'utf8'),
fs.readFileSync(path.join(repoRoot, 'README_EN.md'), 'utf8'),
'README.md and README_EN.md must stay synchronized',
);
});
test('README demos use checked-in captures and live deep links below the existing hero', () => {
const demos = [
{
asset: 'archify-demo-story.png',
link: 'agent-tool-call.workflow.html?theme=dark&present=1&play=1#view=happy-path',
},
{
asset: 'archify-demo-route.png',
link: 'cache-miss.sequence.html?theme=dark&present=1#route=web~db',
},
{
asset: 'archify-demo-lens.png',
link: 'production-deployment.architecture.html?theme=dark&present=1#lens=backend~database',
},
];
for (const demo of demos) {
const buffer = fs.readFileSync(path.join(repoRoot, 'docs', 'assets', demo.asset));
assert.equal(buffer.subarray(1, 4).toString('ascii'), 'PNG', `${demo.asset}: invalid PNG signature`);
assert.equal(buffer.readUInt32BE(16), 1280, `${demo.asset}: unexpected width`);
assert.equal(buffer.readUInt32BE(20), 720, `${demo.asset}: unexpected height`);
assert.ok(buffer.byteLength < 400 * 1024, `${demo.asset}: capture is too large`);
}
for (const filename of ['README.md', 'README_EN.md', 'README_ZH.md']) {
const readme = fs.readFileSync(path.join(repoRoot, filename), 'utf8');
const heroIndex = readme.indexOf('docs/assets/archify-readme-hero.png');
const proofIndex = readme.indexOf('docs/assets/archify-live-proof.gif');
const previewIndex = Math.max(readme.indexOf('## Preview'), readme.indexOf('## 预览'));
const demosIndex = Math.max(readme.indexOf('## See Archify in action'), readme.indexOf('## 看看 Archify 能做什么'));
const quickStartIndex = Math.max(readme.indexOf('## Quick start'), readme.indexOf('## 快速开始'));
assert.ok(heroIndex >= 0 && heroIndex < demosIndex, `${filename}: existing hero proof moved`);
assert.ok(demosIndex < previewIndex && previewIndex < quickStartIndex, `${filename}: demo section is misplaced`);
assert.ok(demosIndex < proofIndex && proofIndex < previewIndex, `${filename}: animated proof is outside the demo section`);
for (const demo of demos) {
assert.match(readme, new RegExp(`docs/assets/${demo.asset.replaceAll('.', '\\.')}`));
assert.ok(readme.includes(demo.link), `${filename}: missing ${demo.link}`);
}
}
});
test('README stays scannable without deleting the visual proof set', () => {
const commonAssets = [
'archify-readme-hero.png',
'archify-live-proof.gif',
'archify-demo-story.png',
'archify-demo-route.png',
'archify-demo-lens.png',
'mco-runtime-share-card.png',
'archify-dark.png',
'archify-light.png',
'archify-menu.png',
'archify-workflow.png',
'archify-sequence.png',
'archify-dataflow.png',
'archify-lifecycle.png',
];
for (const filename of ['README.md', 'README_EN.md', 'README_ZH.md']) {
const readme = fs.readFileSync(path.join(repoRoot, filename), 'utf8');
assert.ok(readme.split('\n').length <= 295, `${filename}: README grew beyond the scannable line budget`);
assert.match(readme, filename === 'README_ZH.md' ? /不需要绑定代码库/ : /No repository is required/);
for (const asset of commonAssets) {
assert.ok(readme.includes(`docs/assets/${asset}`), `${filename}: visual proof ${asset} was removed`);
}
}
const english = fs.readFileSync(path.join(repoRoot, 'README.md'), 'utf8');
const wordCount = english.trim().split(/\s+/).length;
const intro = english.slice(0, english.indexOf('![License]'));
const introBullets = intro.match(/^- \*\*/gm) || [];
assert.ok(wordCount <= 2085, `README.md is too verbose again (${wordCount} words)`);
assert.ok(introBullets.length <= 8, `README.md has too many top-level capability bullets (${introBullets.length})`);
const chinese = fs.readFileSync(path.join(repoRoot, 'README_ZH.md'), 'utf8');
assert.ok(chinese.includes('docs/assets/claude-skills-settings.png'), 'README_ZH.md lost the Claude Skills setup image');
});
test('all README languages end with the self-hosted star history chart', () => {
const lightChart = 'https://raw.githubusercontent.com/tt-a1i/archify/star-history/assets/star-history-light.svg';
const darkChart = 'https://raw.githubusercontent.com/tt-a1i/archify/star-history/assets/star-history-dark.svg';
const workflow = fs.readFileSync(path.join(repoRoot, '.github', 'workflows', 'star-history.yml'), 'utf8');
for (const filename of ['README.md', 'README_EN.md', 'README_ZH.md']) {
const readme = fs.readFileSync(path.join(repoRoot, filename), 'utf8');
const sectionIndex = readme.lastIndexOf('## Star History');
const contributingIndex = Math.max(readme.indexOf('## Contributing'), readme.indexOf('## 参与贡献'));
assert.ok(sectionIndex > contributingIndex, `${filename}: Star History must follow Contributing`);
assert.ok(readme.includes(lightChart), `${filename}: missing light star history chart`);
assert.ok(readme.includes(darkChart), `${filename}: missing dark star history chart`);
assert.equal(readme.trimEnd().endsWith('</p>'), true, `${filename}: Star History must remain the final section`);
}
assert.match(workflow, /permissions:\n contents: write/);
assert.match(workflow, /narayann7\/star-history-action@[0-9a-f]{40}/);
assert.match(workflow, /output-dir: assets/);
assert.match(workflow, /update-readme: ['"]false['"]/);
assert.match(workflow, /commit: ['"]false['"]/);
assert.match(workflow, /bash scripts\/publish-star-history\.sh star-history/);
assert.doesNotMatch(workflow, /branch: star-history/);
assert.doesNotMatch(workflow, /xpzouying\/star-history/);
});
test('Star History publishing advances the data branch without a force push', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-star-history-'));
const remote = path.join(fixture, 'remote.git');
const firstCheckout = path.join(fixture, 'first');
const secondCheckout = path.join(fixture, 'second');
const publisher = path.join(repoRoot, 'scripts', 'publish-star-history.sh');
try {
git(fixture, 'init', '--bare', remote);
git(fixture, '--git-dir', remote, 'config', 'receive.denyNonFastForwards', 'true');
git(fixture, '--git-dir', remote, 'config', 'receive.denyDeletes', 'true');
fs.mkdirSync(firstCheckout);
git(firstCheckout, 'init', '-b', 'main');
git(firstCheckout, 'config', 'user.name', 'Fixture');
git(firstCheckout, 'config', 'user.email', 'fixture@example.com');
fs.writeFileSync(path.join(firstCheckout, 'README.md'), 'fixture\n');
git(firstCheckout, 'add', 'README.md');
git(firstCheckout, 'commit', '-m', 'seed');
git(firstCheckout, 'remote', 'add', 'origin', remote);
git(firstCheckout, 'push', '-u', 'origin', 'main');
const firstTemp = path.join(fixture, 'run-1');
fs.mkdirSync(firstTemp);
writeStarHistoryCharts(firstCheckout, 'v1');
execFileSync('bash', [publisher, 'star-history'], {
cwd: firstCheckout,
env: { ...process.env, RUNNER_TEMP: firstTemp },
});
const firstCommit = git(fixture, '--git-dir', remote, 'rev-parse', 'refs/heads/star-history');
git(fixture, 'clone', '--branch', 'main', remote, secondCheckout);
const secondTemp = path.join(fixture, 'run-2');
fs.mkdirSync(secondTemp);
writeStarHistoryCharts(secondCheckout, 'v2');
execFileSync('bash', [publisher, 'star-history'], {
cwd: secondCheckout,
env: { ...process.env, RUNNER_TEMP: secondTemp },
});
const secondCommit = git(fixture, '--git-dir', remote, 'rev-parse', 'refs/heads/star-history');
assert.notEqual(secondCommit, firstCommit);
git(fixture, '--git-dir', remote, 'merge-base', '--is-ancestor', firstCommit, secondCommit);
assert.deepEqual(
git(fixture, '--git-dir', remote, 'ls-tree', '-r', '--name-only', secondCommit).split('\n'),
['assets/star-history-dark.svg', 'assets/star-history-light.svg'],
);
assert.match(
git(fixture, '--git-dir', remote, 'show', `${secondCommit}:assets/star-history-light.svg`),
/light v2/,
);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
@@ -0,0 +1,209 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { verifyRepositoryEvidence } from '../renderers/shared/repository-evidence.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(skillRoot, '..');
const sourcePath = path.join(repoRoot, 'docs', 'cases', 'mco-runtime.architecture.json');
const artifactPath = path.join(repoRoot, 'docs', 'cases', 'mco-runtime.architecture.html');
const shareCardPath = path.join(repoRoot, 'docs', 'assets', 'mco-runtime-share-card.png');
const experimentSourcePath = path.join(repoRoot, 'experiments', 'mco-showcase', 'mco-runtime.architecture.json');
const experimentArtifactPath = path.join(repoRoot, 'experiments', 'mco-showcase', 'mco-runtime.html');
const cli = path.join(skillRoot, 'bin', 'archify.mjs');
const pinnedSource = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
const pinnedRepository = pinnedSource.meta.repository;
function evidencePayload(html) {
const match = html.match(/<script id="archify-source-evidence-data" type="application\/json">([\s\S]*?)<\/script>/);
assert.ok(match, 'checked-in MCO proof is missing verified repository evidence');
return JSON.parse(match[1]);
}
function connectionLabelGeometry(html) {
return Object.fromEntries([...html.matchAll(
/<g data-detail="context"[^>]*data-edge-id="([^"]+)"[^>]*>[\s\S]*?<text x="([^"]+)" y="([^"]+)"/g,
)].map((match) => [match[1], { x: Number(match[2]), y: Number(match[3]) }]));
}
function automaticMcoRoot() {
const candidate = path.resolve(repoRoot, '..', 'mco');
if (!fs.existsSync(path.join(candidate, '.git'))) return null;
try {
verifyRepositoryEvidence('architecture', pinnedSource, candidate);
return candidate;
} catch {
return null;
}
}
const pinnedMcoRoot = process.env.ARCHIFY_MCO_REPO_ROOT
? path.resolve(process.env.ARCHIFY_MCO_REPO_ROOT)
: automaticMcoRoot();
test('MCO showcase preserves checked-in connection-label geometry', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-mco-showcase-layout-'));
try {
const source = JSON.parse(fs.readFileSync(experimentSourcePath, 'utf8'));
assert.deepEqual(
source.meta.repository,
pinnedRepository,
'MCO case and experiment must pin the same repository revision',
);
delete source.meta.repository;
for (const component of source.components) delete component.sources;
const input = path.join(tmp, 'mco-runtime.architecture.json');
const output = path.join(tmp, 'mco-runtime.html');
fs.writeFileSync(input, `${JSON.stringify(source, null, 2)}\n`);
const rendered = spawnSync(process.execPath, [
cli,
'render',
'architecture',
input,
output,
'--quality',
'showcase',
], { encoding: 'utf8' });
assert.equal(rendered.status, 0, `${rendered.stdout}\n${rendered.stderr}`);
const renderedHtml = fs.readFileSync(output, 'utf8');
const checkedInHtml = fs.readFileSync(experimentArtifactPath, 'utf8');
const renderedLabels = connectionLabelGeometry(renderedHtml);
assert.ok(Object.keys(renderedLabels).length >= 8, 'expected the authored MCO connection labels');
assert.deepEqual(
connectionLabelGeometry(checkedInHtml),
renderedLabels,
'checked-in MCO connection-label geometry drifted from its typed source',
);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test('checked-in MCO artifacts are byte-reproducible from the pinned repository revision', {
skip: pinnedMcoRoot
? false
: `Set ARCHIFY_MCO_REPO_ROOT to a matching ${pinnedRepository.url} clone containing revision ${pinnedRepository.revision.slice(0, 7)}.`,
}, () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-mco-byte-reproduction-'));
try {
const experimentOutput = path.join(tmp, 'mco-runtime.experiment.html');
const experiment = spawnSync(process.execPath, [
cli,
'render',
'architecture',
experimentSourcePath,
experimentOutput,
'--quality',
'showcase',
'--repo-root',
pinnedMcoRoot,
], { encoding: 'utf8' });
assert.equal(experiment.status, 0, `${experiment.stdout}\n${experiment.stderr}`);
assert.equal(
fs.readFileSync(experimentOutput, 'utf8'),
fs.readFileSync(experimentArtifactPath, 'utf8'),
'checked-in MCO showcase drifted from its typed source and verified repository',
);
const caseOutput = path.join(tmp, 'mco-runtime.case.html');
const delivered = spawnSync(process.execPath, [
cli,
'deliver',
'architecture',
sourcePath,
caseOutput,
'--quality',
'showcase',
'--repo-root',
pinnedMcoRoot,
'--json',
], { encoding: 'utf8' });
assert.equal(delivered.status, 0, `${delivered.stdout}\n${delivered.stderr}`);
assert.equal(JSON.parse(delivered.stdout).ok, true);
assert.equal(
fs.readFileSync(caseOutput, 'utf8'),
fs.readFileSync(artifactPath, 'utf8'),
'checked-in MCO case drifted from its typed source and verified repository',
);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test('MCO public proof is source-backed, valid, and linked from every README', () => {
const source = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
assert.equal(source.meta.title, 'MCO Runtime Architecture');
assert.equal(source.meta.quality_profile, 'showcase');
assert.equal(source.meta.animation, 'trace');
assert.deepEqual(source.meta.views.map(view => view.id), [
'dispatch-path',
'answer-evidence',
'durable-sessions',
]);
assert.equal(source.components.length, 13);
assert.equal(source.connections.length, 12);
assert.match(source.components.find((component) => component.id === 'router')?.sublabel || '', /\bdoctor\b/);
assert.match(source.components.find((component) => component.id === 'adapters')?.sublabel || '', /\bdetect\b/);
assert.match(source.meta.repository.url, /^https:\/\/github\.com\/[^/]+\/[^/]+$/);
assert.match(source.meta.repository.revision, /^[0-9a-f]{40}$/);
const references = source.components.reduce((count, component) => count + (component.sources?.length || 0), 0);
assert.equal(references, 13);
const cardCopy = JSON.stringify(source.cards);
assert.ok(cardCopy.includes(`main @ ${source.meta.repository.revision.slice(0, 7)}`));
assert.ok(cardCopy.includes(new URL(source.meta.repository.url).host + new URL(source.meta.repository.url).pathname));
const checkedInHtml = fs.readFileSync(artifactPath, 'utf8');
const evidence = evidencePayload(checkedInHtml);
assert.equal(evidence.verified, true);
assert.equal(evidence.repository.url, source.meta.repository.url);
assert.equal(evidence.repository.revision, source.meta.repository.revision);
assert.equal(evidence.repository.shortRevision, source.meta.repository.revision.slice(0, 7));
assert.equal(evidence.referenceCount, references);
assert.match(checkedInHtml, /Archify\.sourceEvidence\.installBeacons\(\)/);
execFileSync(process.execPath, [cli, 'check', artifactPath], { encoding: 'utf8' });
const noRootTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-mco-proof-no-root-'));
try {
const output = path.join(noRootTmp, 'mco-runtime.html');
const result = spawnSync(process.execPath, [
cli,
'deliver',
'architecture',
sourcePath,
output,
'--quality',
'showcase',
'--json',
], { encoding: 'utf8' });
assert.equal(result.status, 1);
assert.match(JSON.parse(result.stdout).error, /Pass --repo-root/);
assert.equal(fs.existsSync(output), false);
} finally {
fs.rmSync(noRootTmp, { recursive: true, force: true });
}
const png = fs.readFileSync(shareCardPath);
assert.equal(png.subarray(0, 8).toString('hex'), '89504e470d0a1a0a');
assert.equal(png.readUInt32BE(16), 1200);
assert.equal(png.readUInt32BE(20), 630);
assert.ok(png.byteLength > 20_000, 'MCO Share Card is unexpectedly small');
const repositorySlug = new URL(source.meta.repository.url).pathname.replace(/^\/|\/$/g, '');
const shortRevision = source.meta.repository.revision.slice(0, 7);
for (const filename of ['README.md', 'README_EN.md', 'README_ZH.md']) {
const readme = fs.readFileSync(path.join(repoRoot, filename), 'utf8');
assert.match(readme, /docs\/assets\/mco-runtime-share-card\.png/);
assert.match(readme, /cases\/mco-runtime\.architecture\.html\?theme=dark&present=1#view=dispatch-path/);
assert.match(readme, /docs\/cases\/mco-runtime\.architecture\.json/);
assert.ok(readme.includes(`[\`${repositorySlug}\`](${source.meta.repository.url})`), `${filename}: repository link drifted`);
assert.ok(readme.includes(`\`${shortRevision}\``), `${filename}: repository revision drifted`);
}
});
@@ -0,0 +1,115 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } 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 template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-relationship-direct-explorer-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
const result = spawnSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
], { encoding: 'utf8' });
return { result, html: fs.existsSync(output) ? fs.readFileSync(output, 'utf8') : '' };
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all five renderers inherit one viewer-only Direct Relationship Explorer', () => {
for (const [mode, example] of Object.entries(CASES)) {
const { result, html } = render(mode, example);
assert.equal(result.status, 0, result.stderr);
assert.match(html, /function installRelationshipHitTargets\(\)/, mode);
assert.match(html, /data-relationship-hit-overlay/, mode);
assert.match(html, /className \|\| 'relationship-hit-rail'/, mode);
assert.doesNotMatch(canonicalSvg(html), /relationship-hit-(?:overlay|target|rail)|data-relationship-direct-active/, mode);
}
});
test('one roving target represents each exact stable edge key and authored direction', () => {
assert.match(template, /function relationshipHitRecords\(\)/);
assert.match(template, /var recordsByKey = \{\}/);
assert.match(template, /existing\.invalid = true/);
assert.match(template, /filter\(function \(record\) \{ return !record\.invalid/);
assert.match(template, /data-relationship-key/);
assert.match(template, /data-relationship-from/);
assert.match(template, /data-relationship-to/);
assert.match(template, /target\.setAttribute\('role', 'button'\)/);
assert.match(template, /relationshipHitOverlay\.setAttribute\('role', 'group'\)/);
assert.match(template, /target\.setAttribute\('aria-describedby', relationshipHelp\.id\)/);
assert.match(template, /target\.setAttribute\('tabindex', index === 0 \? '0' : '-1'\)/);
assert.match(template, /viewerText\('viewer\.passport\.relationship\.inspect'/);
assert.match(template, /relationshipEdgeShapes\(edge\)/);
assert.match(template, /shape\.cloneNode\(false\)/);
assert.match(template, /\.relationship-hit-rail \{[\s\S]*stroke: transparent;[\s\S]*stroke-width: 24/);
});
test('fine-pointer and keyboard intent preview the exact edge before activation', () => {
assert.match(template, /function directRelationshipBlocked\(\)/);
assert.match(template, /function scheduleDirectRelationshipPreview\(target\)/);
assert.match(template, /if \(pinnedRelationshipKey \|\| hoveredRelationship !== target/);
assert.match(template, /previewRelationship\(target, \{ direct: true \}\)/);
assert.match(template, /event\.pointerType === 'touch'/);
assert.match(template, /finePointerQuery && !finePointerQuery\.matches/);
assert.match(template, /addEventListener\('pointerover'/);
assert.match(template, /addEventListener\('pointerout'/);
assert.match(template, /addEventListener\('focusin'/);
assert.match(template, /addEventListener\('focusout'/);
assert.match(template, /data-relationship-direct-active/);
assert.match(template, /\.relationship-hit-target:focus-visible \.relationship-focus-rail/);
});
test('activation opens the existing source passport and pins its exact relationship row', () => {
assert.match(template, /function inspectRelationship\(key, options\)/);
assert.match(template, /if \(directPreviewTimer\) window\.clearTimeout\(directPreviewTimer\)/);
assert.match(template, /if \(pinnedRelationshipKey === key\)/);
assert.match(template, /set\(record\.from, \{ toggle: false, updateUrl: false \}\)/);
assert.match(template, /relationshipList\.querySelectorAll\('\[data-relationship-key\]'\)/);
assert.match(template, /pinnedRelationship = row/);
assert.match(template, /pinnedRelationshipKey = key/);
assert.match(template, /data-relationship-pin-active/);
assert.match(template, /function clearRelationshipPreview\(options\)/);
assert.match(template, /clearRelationshipPreview\(\{ clearPin: true \}\)/);
assert.match(template, /copyBtn\.textContent = viewerText\('viewer\.passport\.copyNode'\)/);
assert.match(template, /previewRelationship\(row\)/);
assert.match(template, /inspectRelationship: inspectRelationship/);
assert.match(template, /event\.key !== 'ArrowRight'/);
assert.match(template, /event\.key !== 'ArrowLeft'/);
assert.match(template, /event\.key !== 'Home'/);
assert.match(template, /event\.key !== 'End'/);
assert.match(template, /event\.key === 'Enter' \|\| event\.key === ' '/);
});
test('direct relationship targets support one-tap touch, yield to stronger states, and stay export-clean', () => {
assert.match(template, /html\.getAttribute\('data-embed'\) === 'true'/);
assert.match(template, /svg\.hasAttribute\('data-story-active'\)/);
assert.match(template, /svg\.hasAttribute\('data-route-active'\)/);
assert.match(template, /svg\.hasAttribute\('data-lens-active'\)/);
assert.match(template, /event\.target\.closest\('\[data-relationship-hit-key\]'\)/);
assert.match(template, /@media \(hover: none\), \(pointer: coarse\)[\s\S]*\.relationship-hit-rail \{ stroke-width: 24/);
assert.match(template, /@media print \{[\s\S]*\.relationship-hit-overlay/);
assert.match(template, /clone\.removeAttribute\('data-relationship-direct-active'\)/);
assert.match(template, /clone\.removeAttribute\('data-relationship-pin-active'\)/);
assert.match(template, /clone\.querySelectorAll\('\[data-relationship-hit-overlay\]'\)/);
assert.match(template, /\[data-relationship-hit-overlay\][^']*\[data-relationship-pulse-overlay\]/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,128 @@
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-relationship-lens-'));
const CASES = {
architecture: { example: 'web-app.architecture.json', collection: 'connections' },
workflow: { example: 'agent-tool-call.workflow.json', collection: 'edges' },
sequence: { example: 'cache-miss-request.sequence.json', collection: 'messages' },
dataflow: { example: 'product-analytics.dataflow.json', collection: 'flows' },
lifecycle: { example: 'agent-run.lifecycle.json', collection: 'transitions' },
};
function render(mode, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function svg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
function escapeAttr(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
test('all typed renderers expose named, stable relationships without changing geometry', () => {
for (const [mode, config] of Object.entries(CASES)) {
const html = render(mode, config.example);
const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', config.example), 'utf8'));
const relationships = source[config.collection];
const diagram = svg(html);
const keys = new Set(Array.from(diagram.matchAll(/data-edge-key="(\d+)"/g), (match) => match[1]));
assert.equal(keys.size, relationships.length, `${mode} keeps one stable key per source relationship`);
relationships.forEach((relationship, index) => {
const expectedKey = source.schema_version === 2 ? '\\d+' : String(index);
assert.match(diagram, new RegExp(`data-edge-from="${escapeAttr(relationship.from)}"[^>]+data-edge-to="${escapeAttr(relationship.to)}"[^>]+data-edge-key="${expectedKey}"`), `${mode} relationship ${index}`);
if (relationship.label) {
assert.match(diagram, new RegExp(`data-edge-label="${escapeAttr(relationship.label).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`), `${mode} named relationship ${index}`);
}
});
assert.match(diagram, /data-node-id="[^"]+" data-node-label="[^"]+" tabindex="0"/, mode);
}
});
test('relationship lens groups incoming, outgoing, and self-loop paths and follows neighbors', () => {
const html = render('architecture', CASES.architecture.example);
assert.match(html, /id="focus-chip" hidden role="region" aria-labelledby="relationship-lens-title"/);
assert.match(html, /id="relationship-lens-list" aria-label="Connected relationships"/);
assert.match(html, /function relationshipsFor\(id, byId\)/);
assert.match(html, /direction = from === id && to === id \? 'loop' : \(from === id \? 'out' : 'in'\)/);
assert.match(html, /\{ id: 'out', label: viewerText\('viewer\.passport\.relationship\.group\.out'\) \}/);
assert.match(html, /\{ id: 'in', label: viewerText\('viewer\.passport\.relationship\.group\.in'\) \}/);
assert.match(html, /data-relationship-target/);
assert.match(html, /data-relationship-key/);
assert.match(html, /data-relationship-from/);
assert.match(html, /data-relationship-to/);
assert.match(html, /set\(id, \{ toggle: false \}\)/);
assert.match(html, /Archify\.view\.reveal\(\[id\], \{ includeNeighbors: true, reason: 'relationship' \}\)/);
assert.doesNotMatch(svg(html), /relationship-lens|Connected relationships/);
});
test('relationship preview precisely links pointer and keyboard rows to an edge and its endpoints', () => {
const html = render('sequence', CASES.sequence.example);
const diagram = svg(html);
assert.match(html, /function previewRelationship\(button, options\)/);
assert.match(html, /edge\.getAttribute\('data-edge-key'\) === key/);
assert.match(html, /data-relationship-preview-source/);
assert.match(html, /data-relationship-preview-target/);
assert.match(html, /addEventListener\('pointerover'/);
assert.match(html, /addEventListener\('pointerout'/);
assert.match(html, /addEventListener\('focusin'/);
assert.match(html, /addEventListener\('focusout'/);
assert.match(html, /pinnedRelationship \|\| focusedRelationship \|\| hoveredRelationship/);
assert.doesNotMatch(diagram, /data-relationship-preview(?:-active|-node|-source|-target)?=/);
});
test('relationship preview is export-clean and visually geometry-neutral', () => {
const html = render('dataflow', CASES.dataflow.example);
assert.match(html, /clone\.removeAttribute\('data-relationship-preview-active'\)/);
assert.match(html, /clone\.querySelectorAll\('\[data-relationship-preview\], \[data-relationship-preview-node\], \[data-relationship-preview-source\], \[data-relationship-preview-target\]'\)/);
assert.match(html, /!clone\.hasAttribute\('data-relationship-preview-active'\)/);
assert.match(html, /Relationship Preview is temporary exploration state layered on top of/);
assert.doesNotMatch(html, /data-relationship-preview[^\n{]*\{[^}]*\b(?:x|y|transform)\s*:/);
});
test('relationship lens is keyboard navigable, mobile-pinned, and excluded from embed and print', () => {
const html = render('workflow', CASES.workflow.example);
assert.match(html, /event\.key !== 'ArrowDown'/);
assert.match(html, /event\.key !== 'ArrowUp'/);
assert.match(html, /event\.key !== 'Home'/);
assert.match(html, /event\.key !== 'End'/);
assert.match(html, /buttons\[index\]\.focus\(\)/);
assert.match(html, /data-wide-diagram="true"\] \.focus-chip/);
assert.match(html, /\.focus-chip\[data-relationship-previewing="true"\] \.relationship-lens-list/);
assert.match(html, /\.relationship-lens-row:not\(\[data-preview-active="true"\]\)/);
assert.match(html, /var mobile = window\.innerWidth <= 720/);
assert.match(html, /previewingOnMobile = mobile && chip\.getAttribute\('data-relationship-previewing'\) === 'true'/);
assert.match(html, /nodeCenter < \(visibleTop \+ visibleBottom\) \/ 2 \? pinnedBottom : pinnedTop/);
assert.match(html, /html\[data-embed="true"\] \.focus-chip/);
assert.match(html, /\.toolbar, \.diagram-nav, \.focus-chip, \.guided-views/);
assert.match(html, /chip\.hidden = options\.hideChip === true \|\| normalized\.length !== 1 \|\| selectionMode/);
assert.match(html, /event\.target\.closest\('\.diagram-nav, \.focus-chip, \.node-finder, \.diagram-guide, \.overview-map, \.route-probe, \.semantic-lens'\)/);
assert.match(html, /function placeRelationshipLens\(\)/);
assert.match(html, /visibleTop = Math\.max\(padding, -containerRect\.top \+ padding\)/);
assert.match(html, /window\.addEventListener\('scroll', requestLensPlacement, \{ passive: true \}\)/);
assert.match(html, /container\.addEventListener\('scroll', requestLensPlacement, \{ passive: true \}\)/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,130 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } 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-relationship-permalink-'));
const CASES = {
architecture: { example: 'web-app.architecture.json', collection: 'connections' },
workflow: { example: 'agent-tool-call.workflow.json', collection: 'edges' },
sequence: { example: 'cache-miss-request.sequence.json', collection: 'messages' },
dataflow: { example: 'product-analytics.dataflow.json', collection: 'flows' },
lifecycle: { example: 'agent-run.lifecycle.json', collection: 'transitions' },
};
function fixture(mode) {
return JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', CASES[mode].example), 'utf8'));
}
function run(mode, doc, suffix) {
const input = path.join(tmp, `${mode}-${suffix}.json`);
const output = path.join(tmp, `${mode}-${suffix}.html`);
fs.writeFileSync(input, JSON.stringify(doc));
const result = spawnSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`), input, output,
], { encoding: 'utf8' });
return { result, html: fs.existsSync(output) ? fs.readFileSync(output, 'utf8') : '' };
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
function relationshipKey(html, id) {
return html.match(new RegExp(`data-edge-key="(\\d+)" data-edge-id="${id}"`))?.[1] ?? null;
}
test('all typed renderers preserve optional authored relationship ids beside runtime keys', () => {
for (const mode of Object.keys(CASES)) {
const doc = fixture(mode);
doc[CASES[mode].collection][0].id = 'shareable-relation';
const { result, html } = run(mode, doc, 'stable-id');
assert.equal(result.status, 0, `${mode}: ${result.stderr}`);
assert.match(html, /data-edge-key="\d+" data-edge-id="shareable-relation"/, mode);
assert.match(canonicalSvg(html), /data-edge-id="shareable-relation"/, mode);
}
});
test('authored relationship identity and readable-v2 compiler keys survive source-order changes', () => {
const original = fixture('workflow');
const reordered = fixture('workflow');
const moved = reordered.edges.shift();
reordered.edges.splice(1, 0, moved);
const first = run('workflow', original, 'original-order');
const second = run('workflow', reordered, 'reordered');
assert.equal(first.result.status, 0, first.result.stderr);
assert.equal(second.result.status, 0, second.result.stderr);
assert.notEqual(relationshipKey(first.html, 'request-chat'), null);
assert.equal(
relationshipKey(second.html, 'request-chat'),
relationshipKey(first.html, 'request-chat'),
);
assert.match(first.html, /'#relation=' \+ encodeURIComponent\(record\.id\)/);
assert.match(second.html, /'#relation=' \+ encodeURIComponent\(record\.id\)/);
});
test('relationship ids stay optional and duplicate ids fail closed in the shared zero-install path', () => {
for (const mode of Object.keys(CASES)) {
const idless = fixture(mode);
delete idless[CASES[mode].collection][0].id;
const plain = run(mode, idless, 'idless');
assert.equal(plain.result.status, 0, `${mode}: ${plain.result.stderr}`);
const keyZeroTags = Array.from(plain.html.matchAll(/<(?:path|g)\b[^>]*data-edge-key="0"[^>]*>/g), (match) => match[0]);
assert.ok(keyZeroTags.length > 0, `${mode} emits runtime key zero`);
assert.ok(keyZeroTags.every((tag) => !tag.includes('data-edge-id=')), `${mode} does not invent a durable id`);
const duplicate = fixture(mode);
duplicate[CASES[mode].collection][1].id = duplicate[CASES[mode].collection][0].id;
const rejected = run(mode, duplicate, 'duplicate');
assert.notEqual(rejected.result.status, 0, mode);
assert.match(rejected.result.stderr, /Relationship identity validation failed/);
assert.match(rejected.result.stderr, /duplicates relationship id/);
}
});
test('relationship id syntax is schema-checked before viewer output is written', () => {
const doc = fixture('workflow');
doc.edges[0].id = 'not a stable id';
const { result, html } = run('workflow', doc, 'invalid-id');
assert.notEqual(result.status, 0);
assert.match(result.stderr, /\/edges\/0\/id/);
assert.match(result.stderr, /must match pattern/);
assert.equal(html, '');
});
test('the viewer restores and copies stable relation links without exposing numeric keys', () => {
const { result, html } = run('workflow', fixture('workflow'), 'viewer');
assert.equal(result.status, 0, result.stderr);
assert.match(html, /var edgeId = edge\.getAttribute\('data-edge-id'\) \|\| ''/);
assert.match(html, /target\.setAttribute\('data-relationship-id', record\.id\)/);
assert.match(html, /button\.setAttribute\('data-relationship-id', relationship\.id\)/);
assert.match(html, /copyBtn\.textContent = viewerText\('viewer\.passport\.copyRelation'\)/);
assert.match(html, /'#relation=' \+ encodeURIComponent\(record\.id\)/);
assert.match(html, /var relation = params\.get\('relation'\)/);
assert.match(html, /inspectRelationshipById\(relation, \{ updateUrl: false, toggle: false \}\)/);
assert.match(html, /if \(html\.getAttribute\('data-embed'\) === 'true'\) return false/);
assert.match(html, /if \(html\.getAttribute\('data-embed'\) === 'true' \|\|\s*!inspectRelationshipById/);
assert.match(html, /params\.get\('focus'\) \|\| params\.get\('relation'\)/);
assert.match(html, /if \(!reveal\(\)\) requestAnimationFrame\(reveal\)/);
assert.match(html, /inspectRelationshipById: inspectRelationshipById/);
assert.match(html, /id: record\.id \|\| null, key: record\.key/);
assert.doesNotMatch(html, /'#relation=' \+ encodeURIComponent\(record\.key\)/);
});
test('runtime overlays drop durable edge ids while canonical SVG keeps authored identity', () => {
const { result, html } = run('architecture', fixture('architecture'), 'export-boundary');
assert.equal(result.status, 0, result.stderr);
assert.match(canonicalSvg(html), /data-edge-id="users-to-cdn"/);
assert.doesNotMatch(canonicalSvg(html), /data-relationship-hit-overlay|data-relationship-id=/);
assert.ok((html.match(/clone\.removeAttribute\('data-edge-id'\)/g) || []).length >= 6);
assert.match(html, /querySelectorAll\('\[data-relationship-hit-overlay\]'\)/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,122 @@
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-relationship-pulse-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all typed renderers inherit one exact-edge Directional Flow Pulse', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /function renderRelationshipPulse\(key\)/, mode);
assert.match(html, /function relationshipTokenKind\(edge\)/, mode);
assert.match(html, /function relationshipTokenGeometry\(shape, kind, key, options\)/, mode);
assert.match(html, /data-relationship-pulse-overlay/, mode);
assert.match(html, /setAttribute\('class', 'relationship-flow-pulse'\)/, mode);
assert.doesNotMatch(canonicalSvg(html), /relationship-flow-(?:pulse|token)|data-relationship-(?:pulse|token)/, mode);
}
});
test('pulse clones only the previewed authored geometry and keeps source-to-target direction', () => {
const html = render('sequence', CASES.sequence);
assert.match(html, /edge\.getAttribute\('data-edge-key'\) === key/);
assert.match(html, /function relationshipEdgeShapes\(edge\)/);
assert.match(html, /shape\.cloneNode\(false\)/);
assert.match(html, /clone\.removeAttribute\('marker-end'\)/);
assert.match(html, /clone\.removeAttribute\('data-edge-key'\)/);
assert.match(html, /clone\.removeAttribute\('filter'\)/);
assert.match(html, /clone\.setAttribute\('pathLength', '1'\)/);
assert.match(html, /overlay\.setAttribute\('data-relationship-pulse-key', key\)/);
assert.match(html, /function relationshipTokenPath\(shape\)/);
assert.match(html, /tagName === 'path'.+shape\.getAttribute\('d'\)/s);
assert.match(html, /tagName === 'line'/);
assert.match(html, /tagName === 'polyline' && shape\.points/);
assert.match(html, /motion\.setAttribute\('path', pathData\)/);
assert.match(html, /motion\.setAttribute\('rotate', 'auto'\)/);
assert.match(html, /svg\.insertBefore\(overlay, firstNode\)/);
assert.match(html, /stroke-dashoffset: -1/);
});
test('semantic token classification is evidence-based and fail-closed', () => {
const html = render('lifecycle', CASES.lifecycle);
assert.match(html, /a-security.+sourceKind === 'security'.+targetKind === 'failure'.+return 'security'/s);
assert.match(html, /a-dashed.+sourceKind === 'messagebus'.+targetKind === 'messagebus'.+return 'event'/s);
assert.match(html, /sourceKind === 'database' \|\| targetKind === 'database'.+return 'data'/s);
assert.match(html, /targetKind === 'waiting' \|\| targetKind === 'success'.+return 'state'/s);
assert.match(html, /return 'call';/);
assert.doesNotMatch(html, /relationshipTokenKind[\s\S]{0,1800}data-edge-label/);
});
test('semantic tokens use five distinct inline SVG cues on one finite timing owner', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /data-token-kind', kind/);
assert.match(html, /kind === 'data'[\s\S]+kind === 'event'[\s\S]+kind === 'security'[\s\S]+kind === 'state'/);
assert.match(html, /document\.createElementNS\(svgNamespace, 'animateMotion'\)/);
assert.match(html, /motion\.setAttribute\('dur', options\.duration \|\| '1\.2s'\)/);
assert.match(html, /animation: archify-relationship-token-life 1\.2s linear 1 both/);
assert.match(html, /semantic-flow-token-halo/);
assert.match(html, /Archify\.flowTokens = \{/);
assert.match(html, /data-relationship-token-kind', tokenKind/);
assert.match(html, /var tokenAdded = false/);
assert.doesNotMatch(html, /relationship-flow-token[^}]+infinite/);
});
test('pulse is finite, event-owned, preset-aware, touch-safe, and motion-safe', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /animation: archify-relationship-pulse 1\.2s linear 1 both/);
assert.match(html, /@keyframes archify-relationship-token-life/);
assert.doesNotMatch(html, /relationship-flow-pulse[^}]+infinite/);
assert.match(html, /var activeRelationshipPreview = null/);
assert.match(html, /if \(next === activeRelationshipPreview\) return/);
assert.match(html, /event\.pointerType === 'touch'/);
assert.match(html, /finePointerQuery && !finePointerQuery\.matches/);
assert.match(html, /Archify\.motionGovernor && Archify\.motionGovernor\.isPaused\(\)/);
assert.match(html, /document\.hidden/);
assert.match(html, /addEventListener\('animationcancel', finishPulse/);
assert.match(html, /reducedMotionQuery\.addEventListener\('change', syncRelationshipMotionPreference\)/);
assert.match(html, /if \(event\.matches\) removeRelationshipPulse\(\)/);
assert.match(html, /document\.addEventListener\('visibilitychange'/);
assert.match(html, /html\[data-embed="true"\] \.relationship-pulse-overlay/);
assert.match(html, /svg\[data-preset="signal-flow"\] \.relationship-flow-pulse/);
assert.match(html, /svg\[data-preset="blueprint"\] \.relationship-flow-pulse/);
assert.match(html, /@media \(prefers-reduced-motion: reduce\)[\s\S]+\.relationship-pulse-overlay \{ display: none !important; \}/);
});
test('pulse has one owner and never enters print or canonical exports', () => {
const html = render('dataflow', CASES.dataflow);
assert.match(html, /function removeRelationshipPulse\(\)/);
assert.match(html, /svg\.querySelectorAll\('\[data-relationship-pulse-overlay\]'\)/);
assert.match(html, /@media print \{[\s\S]+\.relationship-pulse-overlay \{ display: none !important; \}/);
assert.match(html, /clone\.querySelectorAll\('\[data-relationship-pulse-overlay\]'\)/);
assert.match(html, /\[data-relationship-pulse-overlay\],[^']*\[data-relationship-preview\]/);
assert.doesNotMatch(canonicalSvg(html), /relationship-flow-(?:pulse|token)|data-relationship-(?:pulse|token)/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,504 @@
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import assert from 'node:assert/strict';
const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, '..', '..');
const checker = path.join(repoRoot, 'scripts', 'check-release-identity.mjs');
function writeFile(root, relativePath, content) {
const target = path.join(root, relativePath);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, content);
}
function runCheck(root) {
return spawnSync(process.execPath, [checker, '--root', root], {
encoding: 'utf8',
});
}
function stableUpdateManifest(version) {
return JSON.stringify({
schemaVersion: 1,
skillId: 'archify',
channel: 'stable',
version,
publishedAt: '2026-07-29T00:00:00Z',
source: {
repository: 'https://github.com/tt-a1i/archify',
ref: `v${version}`,
treeSha: 'a'.repeat(40),
},
artifact: { sha256: 'b'.repeat(64) },
summary: 'Published stable release.',
releaseNotes: `https://github.com/tt-a1i/archify/releases/tag/v${version}`,
severity: 'normal',
});
}
function writeValidDevelopmentFixture(root, overrides = {}) {
const version = '2.13.0-dev.0';
const english = [
'![Development Version](https://img.shields.io/badge/version-2.13.0--dev.0-blue)',
'',
`Current development version: \`v${version}\``,
'',
'Raven uses manual ZIP installation: extract archify.zip into `~/.raven/workspace/skills`, which yields `~/.raven/workspace/skills/archify`; Raven is not an agent-switcher target.',
].join('\n');
const chinese = [
'![开发版本](https://img.shields.io/badge/version-2.13.0--dev.0-blue)',
'',
`当前开发版本:\`v${version}\``,
'',
'Raven 使用 ZIP 手动安装:将 archify.zip 解压到 `~/.raven/workspace/skills`,解压后会得到 `~/.raven/workspace/skills/archify`Raven 不属于 Agent 切换器目标。',
].join('\n');
const files = {
'archify/package.json': JSON.stringify({ version }),
'archify/package-lock.json': JSON.stringify({ version, packages: { '': { version } } }),
'archify/skill-release.json': JSON.stringify({
schemaVersion: 1,
skillId: 'archify',
channel: 'development',
version,
source: { repository: 'https://github.com/tt-a1i/archify' },
updateManifestUrl: 'https://tt-a1i.github.io/archify/skill-updates/archify/stable.json',
}),
'docs/skill-updates/archify/stable.json': stableUpdateManifest('2.12.0'),
'archify/SKILL.md': '---\nmetadata:\n version: "2.13"\n---\n',
'archify/assets/template.html': '<meta name="generator" content="archify 2.13.0-dev.0">',
'CHANGELOG.md': [
'# Changelog',
'',
'## [Unreleased]',
'',
`> Development identity: \`v${version}\`. Not a stable release.`,
'',
'### Added',
'- Real unreleased work.',
'',
'## [2.12.0] — 2026-07-23',
'',
].join('\n'),
'README.md': english,
'README_EN.md': english,
'README_ZH.md': chinese,
'scripts/start-template.html': 'development · 开发版 · [[ARCHIFY_VERSION]]',
'scripts/guide-template.html': 'development · 开发版 · [[ARCHIFY_VERSION]]',
'scripts/gallery-template.html': 'development · 开发版 · [[ARCHIFY_VERSION]]',
'docs/index.html': `<span>development · v${version} · 开发版 · 9/9 checks</span><p>Raven manual ZIP / ZIP 手动安装: extract archify.zip into ~/.raven/workspace/skills, which yields ~/.raven/workspace/skills/archify; 将 archify.zip 解压到 ~/.raven/workspace/skills,解压后会得到 ~/.raven/workspace/skills/archify; not an agent-switcher target.</p>`,
'docs/start.html': `<span>development · v${version} · 开发版</span><p>Raven manual ZIP / ZIP 手动安装: extract archify.zip into ~/.raven/workspace/skills, which yields ~/.raven/workspace/skills/archify; 将 archify.zip 解压到 ~/.raven/workspace/skills,解压后会得到 ~/.raven/workspace/skills/archify; not an agent-switcher target.</p>`,
'ROADMAP.md': `The current development line is \`v${version}\`; it contains the work under Changelog Unreleased and is not a stable release.`,
};
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
writeFile(root, relativePath, content);
}
}
function writeValidStableFixture(root, overrides = {}) {
const version = '2.13.0';
const english = [
'![Stable Version](https://img.shields.io/badge/version-2.13.0-blue)',
'',
`Current stable version: \`v${version}\``,
'',
'Raven uses manual ZIP installation: extract archify.zip into `~/.raven/workspace/skills`, which yields `~/.raven/workspace/skills/archify`; Raven is not an agent-switcher target.',
].join('\n');
const chinese = [
'![稳定版本](https://img.shields.io/badge/version-2.13.0-blue)',
'',
`当前稳定版本:\`v${version}\``,
'',
'Raven 使用 ZIP 手动安装:将 archify.zip 解压到 `~/.raven/workspace/skills`,解压后会得到 `~/.raven/workspace/skills/archify`Raven 不属于 Agent 切换器目标。',
].join('\n');
const files = {
'archify/package.json': JSON.stringify({ version }),
'archify/package-lock.json': JSON.stringify({ version, packages: { '': { version } } }),
'archify/skill-release.json': JSON.stringify({
schemaVersion: 1,
skillId: 'archify',
channel: 'stable',
version,
source: { repository: 'https://github.com/tt-a1i/archify' },
updateManifestUrl: 'https://tt-a1i.github.io/archify/skill-updates/archify/stable.json',
}),
'docs/skill-updates/archify/stable.json': stableUpdateManifest(version),
'archify/SKILL.md': '---\nmetadata:\n version: "2.13"\n---\n',
'archify/assets/template.html': '<meta name="generator" content="archify 2.13.0">',
'CHANGELOG.md': [
'# Changelog',
'',
'## [Unreleased]',
'',
'## [2.13.0] — 2026-07-29',
'- Published work.',
'',
].join('\n'),
'README.md': english,
'README_EN.md': english,
'README_ZH.md': chinese,
'scripts/start-template.html': 'stable · 稳定版 · [[ARCHIFY_VERSION]]',
'scripts/guide-template.html': 'stable · 稳定版 · [[ARCHIFY_VERSION]]',
'scripts/gallery-template.html': 'stable · 稳定版 · [[ARCHIFY_VERSION]]',
'docs/index.html': `<span>stable · v${version} · 稳定版 · 9/9 checks</span><p>Raven manual ZIP / ZIP 手动安装: extract archify.zip into ~/.raven/workspace/skills, which yields ~/.raven/workspace/skills/archify; 将 archify.zip 解压到 ~/.raven/workspace/skills,解压后会得到 ~/.raven/workspace/skills/archify; not an agent-switcher target.</p>`,
'docs/start.html': `<span>stable · v${version} · 稳定版</span><p>Raven manual ZIP / ZIP 手动安装: extract archify.zip into ~/.raven/workspace/skills, which yields ~/.raven/workspace/skills/archify; 将 archify.zip 解压到 ~/.raven/workspace/skills,解压后会得到 ~/.raven/workspace/skills/archify; not an agent-switcher target.</p>`,
'ROADMAP.md': `The current stable version is \`v${version}\`.`,
};
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
writeFile(root, relativePath, content);
}
}
test('an empty Unreleased section accepts a coherent stable release identity', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeValidStableFixture(fixture);
const result = runCheck(fixture);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /release identity ok: 2\.13\.0/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('stable release preparation allows only the immediate prior public manifest', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
const changelog = [
'# Changelog',
'',
'## [Unreleased]',
'',
'## [2.13.0] — 2026-07-29',
'- Release being prepared.',
'',
'## [2.12.0] — 2026-07-23',
'- Previously published release.',
'',
].join('\n');
writeValidStableFixture(fixture, {
'CHANGELOG.md': changelog,
'docs/skill-updates/archify/stable.json': stableUpdateManifest('2.12.0'),
});
const prior = runCheck(fixture);
assert.equal(prior.status, 0, prior.stderr);
writeFile(fixture, 'docs/skill-updates/archify/stable.json', stableUpdateManifest('2.11.0'));
const stale = runCheck(fixture);
assert.notEqual(stale.status, 0);
assert.match(stale.stderr, /immediate prior v2\.12\.0/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('the embedded update identity must match the package release exactly', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeValidDevelopmentFixture(fixture, {
'archify/skill-release.json': JSON.stringify({
schemaVersion: 1,
skillId: 'archify',
channel: 'stable',
version: '2.12.0',
source: { repository: 'https://example.com/untrusted/archify' },
updateManifestUrl: 'https://example.com/latest.json',
}),
});
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /archify\/skill-release\.json must identify archify 2\.13\.0-dev\.0 as development/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('the published update manifest must track the newest stable changelog release', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeValidDevelopmentFixture(fixture, {
'docs/skill-updates/archify/stable.json': stableUpdateManifest('2.11.0'),
});
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /stable\.json must describe the newest published stable v2\.12\.0/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('the published update manifest must use a canonical UTC timestamp', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
const manifest = JSON.parse(stableUpdateManifest('2.12.0'));
manifest.publishedAt = '2026-07-29T08:00:00+08:00';
writeValidDevelopmentFixture(fixture, {
'docs/skill-updates/archify/stable.json': JSON.stringify(manifest),
});
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /stable\.json must describe the newest published stable v2\.12\.0/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('package identities reject leading-zero core and prerelease identifiers', () => {
for (const version of ['02.13.0', '2.13.0-dev.01']) {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeValidDevelopmentFixture(fixture, {
'archify/package.json': JSON.stringify({ version }),
});
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /not a supported SemVer identity/, version);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
}
});
test('the newest stable release is selected by SemVer rather than changelog order', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeValidDevelopmentFixture(fixture, {
'CHANGELOG.md': [
'# Changelog',
'',
'## [Unreleased]',
'',
'> Development identity: `v2.13.0-dev.0`. Not a stable release.',
'',
'### Added',
'- Real unreleased work.',
'',
'## [2.11.0] — 2026-07-16',
'',
'## [2.12.0] — 2026-07-23',
'',
].join('\n'),
});
const result = runCheck(fixture);
assert.equal(result.status, 0, result.stderr);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('real Unreleased changes cannot reuse a stable published package identity', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeFile(fixture, 'archify/package.json', JSON.stringify({ version: '2.12.0' }));
writeFile(fixture, 'CHANGELOG.md', [
'# Changelog',
'',
'## [Unreleased]',
'',
'### Added',
'- Real unreleased work.',
'',
'## [2.12.0] — 2026-07-23',
'',
].join('\n'));
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /Unreleased changes require a prerelease package version/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('package, lockfile, Skill metadata, escaped Shields badge, and public docs share one development identity', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeFile(fixture, 'archify/package.json', JSON.stringify({ version: '2.13.0-dev.0' }));
writeFile(fixture, 'archify/package-lock.json', JSON.stringify({
version: '2.12.0',
packages: { '': { version: '2.12.0' } },
}));
writeFile(fixture, 'archify/SKILL.md', '---\nmetadata:\n version: "2.12"\n---\n');
writeFile(fixture, 'CHANGELOG.md', [
'# Changelog',
'',
'## [Unreleased]',
'',
'### Added',
'- Real unreleased work.',
'',
'## [2.12.0] — 2026-07-23',
'',
].join('\n'));
const staleEnglish = [
'![Version](https://img.shields.io/badge/version-2.13.0-blue)',
'',
'Archify 2.12 includes unreleased capabilities.',
].join('\n');
writeFile(fixture, 'README.md', staleEnglish);
writeFile(fixture, 'README_EN.md', staleEnglish);
writeFile(fixture, 'README_ZH.md', '![Version](https://img.shields.io/badge/version-2.13.0-blue)\n\nArchify 2.12 包含未发布能力。\n');
writeFile(fixture, 'docs/index.html', '<span>Agent Skill · v2.12.0</span>');
writeFile(fixture, 'docs/start.html', '<span>Archify v2.12.0</span>');
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /package-lock\.json must match 2\.13\.0-dev\.0/);
assert.match(result.stderr, /SKILL\.md metadata version 2\.12 must map to package 2\.13\.0-dev\.0/);
assert.match(result.stderr, /README\.md must advertise development identity v2\.13\.0-dev\.0/);
assert.match(result.stderr, /docs\/index\.html must advertise development identity v2\.13\.0-dev\.0/);
assert.match(result.stderr, /docs\/start\.html must advertise development identity v2\.13\.0-dev\.0/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('landing proof receipt matches the current nine-check artifact contract', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeValidDevelopmentFixture(fixture, {
'docs/index.html': '<span>development · v2.13.0-dev.0 · 开发版 · 8/8 checks</span><p>Raven manual ZIP / ZIP 手动安装: extract archify.zip into ~/.raven/workspace/skills, which yields ~/.raven/workspace/skills/archify; 将 archify.zip 解压到 ~/.raven/workspace/skills,解压后会得到 ~/.raven/workspace/skills/archify; not an agent-switcher target.</p>',
});
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /docs\/index\.html proof receipt must say 9\/9/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('landing rejects every stale N/N contract count even when 9/9 is also present', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeValidDevelopmentFixture(fixture, {
'docs/index.html': [
'<span>development · v2.13.0-dev.0 · 开发版 · 9/9 checks</span>',
'<span>legacy receipt · 7/7 checks</span>',
'<p>Raven manual ZIP / ZIP 手动安装: extract archify.zip into ~/.raven/workspace/skills, which yields ~/.raven/workspace/skills/archify; 将 archify.zip 解压到 ~/.raven/workspace/skills,解压后会得到 ~/.raven/workspace/skills/archify; not an agent-switcher target.</p>',
].join('\n'),
});
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /every N\/N proof receipt must be exactly 9\/9; found 7\/7/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('Raven stays a truthful manual ZIP install and never becomes a generated agent-switcher command', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeValidDevelopmentFixture(fixture, {
'docs/start.html': [
'<span>development · v2.13.0-dev.0 · 开发版</span>',
'<button data-agent="raven">Raven</button>',
'<pre>npx skills add tt-a1i/archify --agent raven</pre>',
].join('\n'),
});
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /Raven must remain a manual ZIP installation outside the agent switcher/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('Raven instructions reject extracting the archive into the final Skill directory', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
const nestedEnglish = [
'![Development Version](https://img.shields.io/badge/version-2.13.0--dev.0-blue)',
'',
'Current development version: `v2.13.0-dev.0`',
'',
'Raven is manual ZIP only: extract archify.zip into `~/.raven/workspace/skills/archify`; Raven is not an agent-switcher target.',
].join('\n');
writeValidDevelopmentFixture(fixture, {
'README.md': nestedEnglish,
'README_EN.md': nestedEnglish,
});
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /extract archify\.zip into ~\/\.raven\/workspace\/skills, yielding ~\/\.raven\/workspace\/skills\/archify/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('renderer template generator carries the complete package prerelease identity', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeValidDevelopmentFixture(fixture, {
'archify/assets/template.html': '<meta name="generator" content="archify 2.12.0">',
});
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /archify\/assets\/template\.html generator must be archify 2\.13\.0-dev\.0/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('roadmap current identity follows the package release state', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeValidDevelopmentFixture(fixture, {
'ROADMAP.md': 'The current development line is `v2.12.0`; it is not a stable release.',
});
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /ROADMAP\.md must declare the current development line as v2\.13\.0-dev\.0/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('generated public-page templates keep a development marker and version placeholder', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeValidDevelopmentFixture(fixture, {
'scripts/gallery-template.html': 'Proof Lab / 2.12.0',
});
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /scripts\/gallery-template\.html must use \[\[ARCHIFY_VERSION\]\] with development and 开发版 labels/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('stable public-page templates reject development labels on version-bearing fallbacks', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-identity-'));
try {
writeValidStableFixture(fixture, {
'scripts/guide-template.html': [
'<span data-i18n="versionLabel">Scenario guide / development / v[[ARCHIFY_VERSION]]</span>',
"versionLabel:'Scenario guide / stable / v[[ARCHIFY_VERSION]]'",
"versionLabel:'场景指南 / 稳定版 / v[[ARCHIFY_VERSION]]'",
].join('\n'),
});
const result = runCheck(fixture);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /scripts\/guide-template\.html must not label \[\[ARCHIFY_VERSION\]\] as development or 开发版/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
@@ -0,0 +1,499 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { stageCleanSkill } from '../../scripts/stage-clean-skill.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..', '..');
const canonicalZipNodeMajor = 22;
const currentNodeMajor = Number(process.versions.node.split('.')[0]);
const canonicalZipTest = (name, fn) => test(name, {
skip: currentNodeMajor === canonicalZipNodeMajor
? false
: `canonical ZIP builds require Node ${canonicalZipNodeMajor}`,
}, fn);
function workflowStep(workflow, name) {
const marker = ` - name: ${name}`;
const start = workflow.indexOf(marker);
assert.notEqual(start, -1, `workflow is missing the "${name}" step`);
const next = workflow.indexOf('\n - ', start + marker.length);
return workflow.slice(start, next === -1 ? workflow.length : next);
}
function workflowJob(workflow, name) {
const marker = ` ${name}:`;
const start = workflow.indexOf(marker);
assert.notEqual(start, -1, `workflow is missing the "${name}" job`);
const next = workflow.slice(start + marker.length).search(/\n [a-z][a-z0-9-]*:\n/);
return workflow.slice(start, next === -1 ? workflow.length : start + marker.length + next);
}
test('release prevents manifest preannouncement and smokes the exact archive before upload', () => {
const workflow = fs.readFileSync(path.join(repoRoot, '.github', 'workflows', 'release.yml'), 'utf8');
const tagFetch = workflowStep(workflow, 'Fetch exact tag object');
const tagGate = workflowStep(workflow, 'Tag must match package.json version');
const annotatedTagGate = workflowStep(workflow, 'Stable release tag must be annotated');
const publicationOrder = workflowStep(workflow, 'Stable notifier manifest must remain on the previous release');
const build = workflowStep(workflow, 'Build skill archive');
const smoke = workflowStep(workflow, 'Validate the exact release archive without installing dependencies');
const freshness = workflowStep(workflow, 'Committed zip must match the build (same gate as CI)');
const upload = workflowStep(workflow, 'Create GitHub Release with the zip attached');
const followUp = workflowStep(workflow, 'Record stable notifier publication follow-up');
assert.ok(workflow.indexOf(tagFetch) < workflow.indexOf(tagGate), 'the real tag object must be fetched before release identity checks');
assert.ok(workflow.indexOf(tagGate) < workflow.indexOf(publicationOrder), 'tag/version gate must precede the publication-order gate');
assert.ok(workflow.indexOf(tagGate) < workflow.indexOf(annotatedTagGate), 'tag/version gate must precede the annotated-tag gate');
assert.ok(workflow.indexOf(annotatedTagGate) < workflow.indexOf(publicationOrder), 'annotated-tag gate must precede the publication-order gate');
assert.ok(workflow.indexOf(publicationOrder) < workflow.indexOf(build), 'manifest preannouncement must fail before the release build');
assert.ok(workflow.indexOf(build) < workflow.indexOf(smoke), 'release smoke must follow the archive build');
assert.ok(workflow.indexOf(smoke) < workflow.indexOf(freshness), 'release smoke must inspect the built archive before comparison');
assert.ok(workflow.indexOf(freshness) < workflow.indexOf(upload), 'freshness must pass before release upload');
assert.ok(workflow.indexOf(upload) < workflow.indexOf(followUp), 'manifest follow-up must be recorded only after Release creation');
assert.match(tagFetch, /git fetch --force --no-tags origin/);
assert.match(tagFetch, /refs\/tags\/\$\{GITHUB_REF_NAME\}:refs\/tags\/\$\{GITHUB_REF_NAME\}/);
assert.match(tagGate, /require\('\.\/archify\/package\.json'\)\.version/);
assert.match(tagGate, /GITHUB_REF_NAME#v/);
assert.match(annotatedTagGate, /steps\.release-kind\.outputs\.prerelease == 'false'/);
assert.match(annotatedTagGate, /git cat-file -t "refs\/tags\/\$\{GITHUB_REF_NAME\}"/);
assert.match(annotatedTagGate, /stable releases require an annotated tag/);
assert.match(publicationOrder, /compareSemver\(published\.version, releasing\) >= 0/);
assert.match(publicationOrder, /publish the manifest in a follow-up commit/);
assert.match(build, /run: scripts\/build-zip\.sh \/tmp\/archify-built\.zip/);
assert.match(smoke, /unzip -q \/tmp\/archify-built\.zip -d "\$package_root"/);
assert.match(smoke, /node scripts\/package-smoke\.mjs "\$package_root\/archify"/);
assert.doesNotMatch(smoke, /\bnpm\s+(?:ci|install)\b/);
assert.match(freshness, /cmp -s \/tmp\/archify-built\.zip archify\.zip/);
assert.match(upload, /files: archify\.zip/);
assert.match(followUp, /docs\/skill-updates\/archify\/stable\.json/);
});
test('an exact tag fetch restores an annotated object after a SHA-only checkout', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-release-tag-fetch-'));
const source = path.join(fixture, 'source');
const checkout = path.join(fixture, 'checkout');
const runGit = (cwd, args) => spawnSync('git', args, { cwd, encoding: 'utf8' });
try {
fs.mkdirSync(source);
assert.equal(runGit(source, ['init', '--quiet']).status, 0);
assert.equal(runGit(source, ['config', 'user.name', 'Archify Test']).status, 0);
assert.equal(runGit(source, ['config', 'user.email', 'archify@example.invalid']).status, 0);
fs.writeFileSync(path.join(source, 'release.txt'), 'release\n');
assert.equal(runGit(source, ['add', 'release.txt']).status, 0);
assert.equal(runGit(source, ['commit', '--quiet', '-m', 'release fixture']).status, 0);
assert.equal(runGit(source, ['tag', '-a', 'v1.0.0', '-m', 'Release v1.0.0']).status, 0);
const commit = runGit(source, ['rev-parse', 'HEAD']).stdout.trim();
fs.mkdirSync(checkout);
assert.equal(runGit(checkout, ['init', '--quiet']).status, 0);
assert.equal(runGit(checkout, ['remote', 'add', 'origin', source]).status, 0);
assert.equal(runGit(checkout, [
'fetch', '--no-tags', '--depth=1', 'origin',
`+${commit}:refs/tags/v1.0.0`,
]).status, 0);
assert.equal(runGit(checkout, ['cat-file', '-t', 'refs/tags/v1.0.0']).stdout.trim(), 'commit');
assert.equal(runGit(checkout, [
'fetch', '--force', '--no-tags', 'origin',
'+refs/tags/v1.0.0:refs/tags/v1.0.0',
]).status, 0);
assert.equal(runGit(checkout, ['cat-file', '-t', 'refs/tags/v1.0.0']).stdout.trim(), 'tag');
assert.equal(runGit(checkout, ['rev-parse', 'refs/tags/v1.0.0^{}']).stdout.trim(), commit);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('CI binds a public notifier manifest to the Release asset, tagged archive, and tag tree build', () => {
const workflow = fs.readFileSync(path.join(repoRoot, '.github', 'workflows', 'ci.yml'), 'utf8');
const job = workflowJob(workflow, 'published-update-manifest');
assert.match(job, /validateStableUpdateManifest/);
assert.match(job, /releases\/latest/);
assert.match(job, /latest_stable_tag" != "v\$\{manifest_version\}"/);
assert.match(job, /releases\/tags\/v\$\{manifest_version\}/);
assert.match(job, /select\(\.draft == false and \.prerelease == false\)/);
assert.match(job, /select\(\.name == "archify\.zip"\)/);
assert.match(job, /releases\/assets\/\$\{release_asset_id\}/);
assert.match(job, /Accept: application\/octet-stream/);
assert.match(job, /refs\/tags\/v\$\{manifest_version\}:refs\/tags\/v\$\{manifest_version\}/);
assert.match(job, /git show "v\$\{manifest_version\}:archify\.zip" > "\$tagged_archive"/);
assert.match(job, /cmp -s "\$published_archive" "\$tagged_archive"/);
assert.match(job, /check-stable-update-manifest\.mjs/);
assert.match(job, /--archive "\$published_archive"/);
assert.match(job, /--tag "v\$\{manifest_version\}"/);
assert.match(job, /--source-ref "v\$\{manifest_version\}"/);
assert.match(job, /git worktree add --detach "\$tag_checkout" "v\$\{manifest_version\}"/);
assert.match(job, /"\$tag_checkout\/scripts\/build-zip\.sh" "\$rebuilt_archive"/);
assert.match(job, /cmp -s "\$rebuilt_archive" "\$tagged_archive"/);
assert.match(job, /manifest_version" == "2\.15\.0"/);
assert.match(job, /missing the deterministic archive builder/);
});
test('release docs disclose that mutable Release assets are verified only at deployment time', () => {
const design = fs.readFileSync(
path.join(repoRoot, 'docs', 'skill-embedded-optional-update-notifier-design.md'),
'utf8',
);
assert.match(design, /部署时点/);
assert.match(design, /部署后替换[^。]*不会自动触发复验/);
assert.match(design, /immutable release/i);
assert.doesNotMatch(design, /即使 Release 资产后来可被替换,也不能脱离/);
});
test('GitHub Pages deploys docs only after every repository gate succeeds', () => {
const workflow = fs.readFileSync(path.join(repoRoot, '.github', 'workflows', 'ci.yml'), 'utf8');
const job = workflowJob(workflow, 'deploy-pages');
assert.match(job, /if: github\.event_name == 'push' && github\.ref == 'refs\/heads\/main'/);
assert.match(job, /needs: \[test, webm-artifact, zip-freshness, published-update-manifest, package-smoke\]/);
assert.match(job, /pages: write/);
assert.match(job, /id-token: write/);
assert.match(job, /repos\/\$\{GITHUB_REPOSITORY\}\/git\/ref\/heads\/main/);
assert.match(job, /current_main" == "\$GITHUB_SHA"/);
assert.match(job, /Skipping obsolete Pages deployment/);
assert.match(job, /if: steps\.deployment-head\.outputs\.current == 'true'/);
assert.match(job, /actions\/configure-pages@v5/);
assert.match(job, /actions\/upload-pages-artifact@v4/);
assert.match(job, /path: docs/);
assert.match(job, /actions\/deploy-pages@v4/);
});
test('release tags with a SemVer prerelease are marked prerelease and never become latest', () => {
const workflow = fs.readFileSync(path.join(repoRoot, '.github', 'workflows', 'release.yml'), 'utf8');
const classifier = workflowStep(workflow, 'Classify stable and prerelease tags');
const upload = workflowStep(workflow, 'Create GitHub Release with the zip attached');
assert.ok(workflow.indexOf(classifier) < workflow.indexOf(upload), 'release kind must be known before upload');
assert.match(classifier, /version="\$\{GITHUB_REF_NAME#v\}"/);
assert.match(classifier, /validateLocalRelease/);
assert.match(classifier, /update-contract\.mjs/);
assert.match(classifier, /release\.version !== process\.argv\[1\]/);
assert.match(classifier, /if \[\[ "\$channel" == "development" \]\]/);
assert.match(classifier, /echo "prerelease=true" >> "\$GITHUB_OUTPUT"/);
assert.match(classifier, /echo "make_latest=false" >> "\$GITHUB_OUTPUT"/);
assert.match(classifier, /echo "prerelease=false" >> "\$GITHUB_OUTPUT"/);
assert.match(classifier, /echo "make_latest=true" >> "\$GITHUB_OUTPUT"/);
assert.match(upload, /prerelease: \$\{\{ steps\.release-kind\.outputs\.prerelease \}\}/);
assert.match(upload, /make_latest: \$\{\{ steps\.release-kind\.outputs\.make_latest \}\}/);
});
test('package smoke rejects every dependency or repository-only artifact', () => {
const packageSmoke = path.join(repoRoot, 'scripts', 'package-smoke.mjs');
const forbidden = [
{ relative: 'node_modules', kind: 'directory' },
{ relative: 'package-lock.json', kind: 'file' },
{ relative: path.join('scripts', 'generate-validators.mjs'), kind: 'file' },
{ relative: 'test', kind: 'directory' },
{ relative: '.hive', kind: 'directory' },
{ relative: '.workbuddy', kind: 'directory' },
];
for (const { relative, kind } of forbidden) {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-package-gate-'));
try {
fs.mkdirSync(path.join(fixture, 'bin'), { recursive: true });
fs.writeFileSync(path.join(fixture, 'bin', 'archify.mjs'), '');
const target = path.join(fixture, relative);
if (kind === 'directory') fs.mkdirSync(target, { recursive: true });
else {
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, '');
}
const result = spawnSync(process.execPath, [packageSmoke, fixture], { encoding: 'utf8' });
assert.notEqual(result.status, 0, `${relative} must fail package smoke`);
assert.match(
`${result.stdout}\n${result.stderr}`,
new RegExp(`packaged skill must not contain ${relative.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`),
`${relative} must be rejected explicitly`,
);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
}
});
test('package smoke verifies the embedded notifier identity and local disable switch', () => {
const source = fs.readFileSync(path.join(repoRoot, 'scripts', 'package-smoke.mjs'), 'utf8');
assert.match(source, /scripts', 'check-update\.mjs/);
assert.match(source, /scripts', 'update-contract\.mjs/);
assert.match(source, /skill-release\.json/);
assert.match(source, /ARCHIFY_UPDATE_CHECK_DISABLED: '1'/);
assert.match(source, /reason !== 'disabled'/);
});
test('package smoke increments an arbitrary-precision SemVer patch without Number coercion', () => {
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-package-bigint-version-'));
const skillRoot = path.join(scratch, 'archify');
try {
stageCleanSkill({ repoRoot, destination: skillRoot });
const packagePath = path.join(skillRoot, 'package.json');
const releasePath = path.join(skillRoot, 'skill-release.json');
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
const release = JSON.parse(fs.readFileSync(releasePath, 'utf8'));
const version = '2.16.9007199254740993';
packageJson.version = version;
release.version = version;
release.channel = 'stable';
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
fs.writeFileSync(releasePath, `${JSON.stringify(release, null, 2)}\n`);
const smoke = spawnSync(process.execPath, [path.join(repoRoot, 'scripts/package-smoke.mjs'), skillRoot], {
cwd: repoRoot,
encoding: 'utf8',
});
assert.equal(smoke.status, 0, smoke.stderr || smoke.stdout);
} finally {
fs.rmSync(scratch, { recursive: true, force: true });
}
});
test('archive build refuses to silently omit required notifier files', () => {
const buildSource = fs.readFileSync(path.join(repoRoot, 'scripts', 'build-zip.sh'), 'utf8');
const stageSource = fs.readFileSync(path.join(repoRoot, 'scripts', 'stage-clean-skill.mjs'), 'utf8');
assert.match(buildSource, /stage-clean-skill\.mjs/);
assert.match(stageSource, /archify\/skill-release\.json/);
assert.match(stageSource, /archify\/scripts\/check-update\.mjs/);
assert.match(stageSource, /archify\/scripts\/update-contract\.mjs/);
assert.match(stageSource, /git', \['ls-files', '--stage', '-z'/);
assert.match(stageSource, /required package input is not tracked by Git/);
});
canonicalZipTest('package smoke rejects every dependency metadata field in a built package', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-built-package-gate-'));
try {
const archive = path.join(fixture, 'archify.zip');
const build = spawnSync(path.join(repoRoot, 'scripts', 'build-zip.sh'), [archive], {
cwd: repoRoot,
encoding: 'utf8',
});
assert.equal(build.status, 0, `${build.stdout}\n${build.stderr}`);
const extracted = path.join(fixture, 'extracted');
fs.mkdirSync(extracted);
const unzip = spawnSync('unzip', ['-q', archive, '-d', extracted], { encoding: 'utf8' });
assert.equal(unzip.status, 0, `${unzip.stdout}\n${unzip.stderr}`);
const builtPackage = path.join(extracted, 'archify');
const dependencyFields = {
dependencies: { runtime: '1.0.0' },
devDependencies: { build: '1.0.0' },
optionalDependencies: { optional: '1.0.0' },
peerDependencies: { peer: '1.0.0' },
bundledDependencies: ['bundled'],
bundleDependencies: ['bundle-alias'],
};
for (const [field, value] of Object.entries(dependencyFields)) {
const caseRoot = path.join(fixture, field);
fs.cpSync(builtPackage, caseRoot, { recursive: true });
const packagePath = path.join(caseRoot, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
packageJson[field] = value;
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
const result = spawnSync(process.execPath, [path.join(repoRoot, 'scripts', 'package-smoke.mjs'), caseRoot], {
encoding: 'utf8',
});
assert.notEqual(result.status, 0, `${field} must fail package smoke`);
assert.match(`${result.stdout}\n${result.stderr}`, new RegExp(`dependency metadata: ${field}\\b`));
}
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
canonicalZipTest('built archives contain the embedded notifier runtime', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-notifier-package-gate-'));
try {
const archive = path.join(fixture, 'archify.zip');
const build = spawnSync(path.join(repoRoot, 'scripts', 'build-zip.sh'), [archive], {
cwd: repoRoot,
encoding: 'utf8',
});
assert.equal(build.status, 0, `${build.stdout}\n${build.stderr}`);
const listing = spawnSync('unzip', ['-Z1', archive], { encoding: 'utf8' });
assert.equal(listing.status, 0, `${listing.stdout}\n${listing.stderr}`);
const entries = new Set(listing.stdout.trim().split('\n'));
assert.ok(entries.has('archify/skill-release.json'));
assert.ok(entries.has('archify/scripts/check-update.mjs'));
assert.ok(entries.has('archify/scripts/update-contract.mjs'));
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
canonicalZipTest('archive build excludes untracked files and external symlinks from the live working tree', () => {
const marker = `.package-negative-${process.pid}-${Date.now()}`;
const untracked = path.join(repoRoot, 'archify', `${marker}.txt`);
const externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-package-external-'));
const externalTarget = path.join(externalRoot, 'secret.txt');
const externalLink = path.join(repoRoot, 'archify', `${marker}.link`);
const outputRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-package-negative-'));
const archive = path.join(outputRoot, 'archify.zip');
try {
fs.writeFileSync(untracked, 'must not ship\n');
fs.writeFileSync(externalTarget, 'external content must not ship\n');
fs.symlinkSync(externalTarget, externalLink, 'file');
const build = spawnSync(path.join(repoRoot, 'scripts', 'build-zip.sh'), [archive], {
cwd: repoRoot,
encoding: 'utf8',
});
assert.equal(build.status, 0, `${build.stdout}\n${build.stderr}`);
const listing = spawnSync('unzip', ['-Z1', archive], { encoding: 'utf8' });
assert.equal(listing.status, 0, `${listing.stdout}\n${listing.stderr}`);
assert.doesNotMatch(listing.stdout, new RegExp(marker), 'untracked files and symlinks must not enter the archive');
} finally {
fs.rmSync(untracked, { force: true });
fs.rmSync(externalLink, { force: true });
fs.rmSync(externalRoot, { recursive: true, force: true });
fs.rmSync(outputRoot, { recursive: true, force: true });
}
});
canonicalZipTest('archive build rejects an unmerged index and preserves an existing archive', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-package-unmerged-'));
const scripts = path.join(fixture, 'scripts');
const skill = path.join(fixture, 'archify');
const license = path.join(skill, 'LICENSE');
const archive = path.join(fixture, 'trusted.zip');
const trusted = Buffer.from('trusted archive bytes');
const git = (args, options = {}) => spawnSync('git', args, {
cwd: fixture,
encoding: 'utf8',
...options,
});
try {
fs.mkdirSync(path.join(skill, 'renderers', 'shared'), { recursive: true });
fs.mkdirSync(path.join(skill, 'scripts'), { recursive: true });
fs.mkdirSync(scripts);
fs.copyFileSync(path.join(repoRoot, 'scripts', 'build-zip.sh'), path.join(scripts, 'build-zip.sh'));
fs.copyFileSync(
path.join(repoRoot, 'scripts', 'write-deterministic-zip.mjs'),
path.join(scripts, 'write-deterministic-zip.mjs'),
);
fs.copyFileSync(
path.join(repoRoot, 'scripts', 'stage-clean-skill.mjs'),
path.join(scripts, 'stage-clean-skill.mjs'),
);
fs.writeFileSync(path.join(skill, 'renderers', 'shared', 'generated-validators.mjs'), 'export default {};\n');
fs.writeFileSync(path.join(skill, 'scripts', 'check-update.mjs'), 'export {};\n');
fs.writeFileSync(path.join(skill, 'scripts', 'update-contract.mjs'), 'export {};\n');
fs.writeFileSync(path.join(skill, 'skill-release.json'), '{}\n');
fs.writeFileSync(path.join(skill, 'package.json'), '{"name":"archify"}\n');
fs.writeFileSync(license, 'base\n');
assert.equal(git(['init']).status, 0);
assert.equal(git(['add', '.']).status, 0);
const base = git(['hash-object', '-w', '--stdin'], { input: 'base\n' });
const ours = git(['hash-object', '-w', '--stdin'], { input: 'ours\n' });
const theirs = git(['hash-object', '-w', '--stdin'], { input: 'theirs\n' });
for (const result of [base, ours, theirs]) assert.equal(result.status, 0, result.stderr);
const indexInfo = [
`100644 ${base.stdout.trim()} 1\tarchify/LICENSE`,
`100644 ${ours.stdout.trim()} 2\tarchify/LICENSE`,
`100644 ${theirs.stdout.trim()} 3\tarchify/LICENSE`,
'',
].join('\n');
assert.equal(git(['update-index', '--index-info'], { input: indexInfo }).status, 0);
fs.writeFileSync(license, '<<<<<<< ours\n=======\n>>>>>>> theirs\n');
fs.writeFileSync(archive, trusted);
const build = spawnSync('bash', [path.join(scripts, 'build-zip.sh'), archive], {
cwd: fixture,
encoding: 'utf8',
});
assert.notEqual(build.status, 0, `${build.stdout}\n${build.stderr}`);
assert.match(build.stderr, /refusing to package unmerged index entry/);
assert.ok(fs.readFileSync(archive).equals(trusted), 'a failed build must preserve the trusted archive');
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('archive build rejects non-canonical Node versions before publishing output', {
skip: currentNodeMajor === canonicalZipNodeMajor
? `requires a Node major other than ${canonicalZipNodeMajor}`
: false,
}, () => {
const outputRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-package-node-version-'));
try {
const archive = path.join(outputRoot, 'archify.zip');
const trusted = Buffer.from('existing canonical archive');
fs.writeFileSync(archive, trusted);
const build = spawnSync(path.join(repoRoot, 'scripts', 'build-zip.sh'), [archive], {
cwd: repoRoot,
encoding: 'utf8',
});
assert.notEqual(build.status, 0, `${build.stdout}\n${build.stderr}`);
assert.match(build.stderr, /canonical archify\.zip builds require Node 22/);
assert.ok(fs.readFileSync(archive).equals(trusted), 'version rejection must preserve the canonical archive');
} finally {
fs.rmSync(outputRoot, { recursive: true, force: true });
}
});
canonicalZipTest('archive build is byte-for-byte reproducible across caller time zones without system zip', () => {
const outputRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-package-reproducible-'));
const utcArchive = path.join(outputRoot, 'utc.zip');
const honoluluArchive = path.join(outputRoot, 'honolulu.zip');
try {
for (const [archive, timezone] of [
[utcArchive, 'UTC'],
[honoluluArchive, 'Pacific/Honolulu'],
]) {
const build = spawnSync(path.join(repoRoot, 'scripts', 'build-zip.sh'), [archive], {
cwd: repoRoot,
encoding: 'utf8',
env: { ...process.env, TZ: timezone },
});
assert.equal(build.status, 0, `${build.stdout}\n${build.stderr}`);
}
assert.ok(
fs.readFileSync(utcArchive).equals(fs.readFileSync(honoluluArchive)),
'identical tracked inputs must produce identical archive bytes',
);
assert.ok(
fs.readFileSync(utcArchive).equals(fs.readFileSync(path.join(repoRoot, 'archify.zip'))),
'the canonical archive toolchain must reproduce the committed archive bytes',
);
assert.deepEqual(
fs.readdirSync(outputRoot).sort(),
['honolulu.zip', 'utc.zip'],
'successful archive publication must not leave temporary files behind',
);
} finally {
fs.rmSync(outputRoot, { recursive: true, force: true });
}
});
test('CI tests the declared Node floor plus every maintained current lane', () => {
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'archify', 'package.json'), 'utf8'));
assert.equal(packageJson.engines?.node, '>=18');
const workflow = fs.readFileSync(path.join(repoRoot, '.github', 'workflows', 'ci.yml'), 'utf8');
const testJob = workflowJob(workflow, 'test');
const versions = testJob.match(/node-version:\s*\[([^\]]+)\]/)?.[1]
.split(',')
.map((version) => Number(version.trim()));
assert.ok(versions, 'test job must declare an explicit Node version matrix');
for (const version of [18, 20, 22, 24]) {
assert.ok(versions.includes(version), `test matrix must cover Node ${version}`);
}
const packageSmokeJob = workflowJob(workflow, 'package-smoke');
assert.match(packageSmokeJob, /os:\s*\[ubuntu-latest, macos-latest, windows-latest\]/);
assert.match(packageSmokeJob, /node-version:\s*22/);
});
@@ -0,0 +1,435 @@
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-output-checks-'));
const checker = path.join(skillRoot, 'scripts/check-render-output.mjs');
function checkHtml(name, svgBody, profile = 'standard', viewBox = '0 0 240 160') {
const htmlPath = path.join(tmp, `${name}.html`);
fs.writeFileSync(htmlPath, `<!doctype html><html><body><svg viewBox="${viewBox}" data-quality-profile="${profile}">${svgBody}</svg></body></html>`);
try {
const stdout = execFileSync('node', [checker, htmlPath], { encoding: 'utf8' });
return { code: 0, result: JSON.parse(stdout) };
} catch (err) {
return { code: err.status ?? 1, result: JSON.parse(String(err.stdout || '{}')) };
}
}
test('render output check: showcase rejects node copy that becomes illegible at 1440px', () => {
const { code, result } = checkHtml('showcase-desktop-readability', `
<g data-node-id="tool-runtime">
<rect x="1398" y="266" width="194" height="70" rx="6" class="c-mask"/>
<text data-detail-anchor x="1495" y="299" class="t-primary" font-size="11">ToolRuntime</text>
<text data-detail="context" x="1495" y="315" class="t-muted" font-size="8.1">permissions and recovery</text>
</g>
`, 'showcase', '0 0 1994 804');
assert.notEqual(code, 0);
const issue = result.composition.issues.find(
(item) => item.code === 'composition/desktop-readability',
);
assert.equal(issue?.severity, 'error');
assert.equal(issue?.viewportWidth, 1440);
assert.ok(issue?.projectedFontPx < issue?.minimumProjectedFontPx);
});
test('render output check: compares exact projected size before rounding diagnostics', () => {
const sourceFontPx = 8.1;
const viewBoxWidth = 1260;
assert.ok(sourceFontPx * 930 / viewBoxWidth < 6);
const { code, result } = checkHtml('showcase-desktop-readability-borderline', `
<g data-node-id="tool-runtime">
<rect x="100" y="100" width="194" height="70" rx="6" class="c-mask"/>
<text data-detail="context" x="197" y="140" class="t-muted" font-size="${sourceFontPx}">permissions and recovery</text>
</g>
`, 'showcase', `0 0 ${viewBoxWidth} 804`);
assert.notEqual(code, 0);
const issue = result.composition.issues.find(
(item) => item.code === 'composition/desktop-readability',
);
assert.equal(issue?.severity, 'error');
assert.ok(issue?.projectedFontPx < issue?.minimumProjectedFontPx);
});
test('render output check: includes primary node labels in desktop readability', () => {
const { code, result } = checkHtml('showcase-primary-desktop-readability', `
<g data-node-id="compact-node">
<rect x="100" y="100" width="120" height="48" rx="6" class="c-mask"/>
<text data-node-label x="160" y="126" class="t-primary" font-size="8">Compact node</text>
</g>
`, 'showcase', '0 0 1300 700');
assert.notEqual(code, 0);
const issue = result.composition.issues.find(
(item) => item.code === 'composition/desktop-readability',
);
assert.equal(issue?.text, 'Compact node');
assert.equal(issue?.detail, 'primary');
assert.equal(issue?.availableDiagramWidth, 930);
assert.ok(issue?.projectedFontPx < issue?.minimumProjectedFontPx);
});
test('render output check: includes semantic boundary labels in desktop readability', () => {
const { code, result } = checkHtml('showcase-boundary-desktop-readability', `
<g data-graph-role="structural-frame-label">
<rect data-graph-role="structural-frame-label-mask" x="100" y="100" width="180" height="16" class="c-mask"/>
<text x="104" y="113" class="t-cloud" font-size="8.4" data-boundary-label>Disaster recovery boundary</text>
</g>
`, 'showcase', '0 0 1376 728');
assert.notEqual(code, 0);
const issue = result.composition.issues.find(
(item) => item.code === 'composition/desktop-readability',
);
assert.equal(issue?.text, 'Disaster recovery boundary');
assert.equal(issue?.detail, 'boundary');
assert.ok(issue?.projectedFontPx < issue?.minimumProjectedFontPx);
});
test('render output check: accepts orthogonal arrows away from legend', () => {
const { code, result } = checkHtml('clean', `
<path d="M 20 20 L 120 20 L 120 60" class="a-default" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<!-- Legend -->
<text x="40" y="120" class="t-primary" font-size="10">Legend</text>
<rect x="40" y="132" width="14" height="9" class="c-backend"/>
<text x="60" y="140" class="t-muted" font-size="7">Backend</text>
`);
assert.equal(code, 0);
assert.equal(result.ok, true);
});
test('render output check: rejects two-point diagonal arrows', () => {
const { code, result } = checkHtml('diagonal', `
<path d="M 20 20 L 120 80" class="a-default" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<!-- Legend -->
<text x="40" y="120" class="t-primary" font-size="10">Legend</text>
`);
assert.notEqual(code, 0);
const check = result.checks.find((item) => item.name === 'orthogonal_arrows');
assert.equal(check.ok, false);
assert.match(check.details[0], /path 1/);
});
test('render output check: rejects a diagonal segment inside a polyline', () => {
const { code, result } = checkHtml('polyline-diagonal', `
<path d="M 20 20 L 60 35 L 120 35" class="a-default" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<!-- Legend -->
<text x="40" y="120" class="t-primary" font-size="10">Legend</text>
`);
assert.notEqual(code, 0);
const check = result.checks.find((item) => item.name === 'orthogonal_arrows');
assert.equal(check.ok, false);
assert.match(check.details[0], /path 1/);
assert.match(check.details[0], /segment 1/);
});
test('render output check: rejects arrows crossing legend text', () => {
const { code, result } = checkHtml('legend-crossing', `
<path d="M 20 112 L 180 112" class="a-dashed" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
<!-- Legend -->
<text x="40" y="120" class="t-primary" font-size="10">Legend</text>
<rect x="40" y="132" width="14" height="9" class="c-backend"/>
<text x="60" y="140" class="t-muted" font-size="7">Backend</text>
`);
assert.notEqual(code, 0);
const check = result.checks.find((item) => item.name === 'legend_clearance');
assert.equal(check.ok, false);
assert.match(check.details[0], /Legend/);
});
test('render output check: ignores unmarked sequence lifelines near legend', () => {
const { code, result } = checkHtml('lifeline-near-legend', `
<path d="M 60 20 L 60 126" class="a-default" stroke-width="0.8" stroke-dasharray="3,7"/>
<!-- Legend -->
<text x="40" y="120" class="t-primary" font-size="10">Legend</text>
<path d="M 120 136 L 154 136" class="a-default" stroke-width="1.4" stroke-dasharray="3,5" marker-end="url(#arrowhead)"/>
<text x="163" y="139" class="t-muted" font-size="8">return</text>
`);
assert.equal(code, 0);
assert.equal(result.ok, true);
});
test('render output check: standard records a proper X as a composition warning', () => {
const { code, result } = checkHtml('standard-crossing', `
<path data-edge-from="a" data-edge-to="b" d="M 20 60 L 200 60" class="a-default" marker-end="url(#arrowhead)"/>
<path data-edge-from="c" data-edge-to="d" d="M 103 20 L 103 120" class="a-dashed" marker-end="url(#arrowhead-dashed)"/>
`);
assert.equal(code, 0);
assert.equal(result.composition.profile, 'standard');
assert.deepEqual(result.composition.summary, { errors: 0, warnings: 1 });
assert.equal(result.composition.metrics.properCrossings, 1);
assert.equal(result.composition.issues[0].code, 'composition/proper-crossing');
assert.equal(result.composition.issues[0].severity, 'warning');
});
test('render output check: showcase rejects a proper X with semantic identities', () => {
const { code, result } = checkHtml('showcase-crossing', `
<path data-edge-id="left" data-edge-from="a" data-edge-to="b" d="M 20 60 L 200 60" class="a-default" marker-end="url(#arrowhead)"/>
<path data-edge-id="right" data-edge-from="c" data-edge-to="d" d="M 100 20 L 100 120" class="a-dashed" marker-end="url(#arrowhead-dashed)"/>
`, 'showcase');
assert.notEqual(code, 0);
const check = result.checks.find((item) => item.name === 'relationship_crossings');
assert.equal(check.ok, false);
assert.match(check.details[0], /\[composition\/proper-crossing\] showcase/);
assert.match(check.details[0], /relationship id "left"/);
assert.deepEqual(result.composition.summary, { errors: 1, warnings: 0 });
});
test('render output check: shared endpoints and endpoint touches pass showcase', () => {
const { code, result } = checkHtml('showcase-exemptions', `
<path data-edge-from="a" data-edge-to="b" d="M 20 60 L 200 60" class="a-default" marker-end="url(#arrowhead)"/>
<path data-edge-from="a" data-edge-to="c" d="M 100 20 L 100 90" class="a-dashed" marker-end="url(#arrowhead-dashed)"/>
<path data-edge-from="d" data-edge-to="e" d="M 20 100 L 100 100" class="a-default" marker-end="url(#arrowhead)"/>
<path data-edge-from="f" data-edge-to="g" d="M 100 100 L 100 140" class="a-default" marker-end="url(#arrowhead)"/>
`, 'showcase');
assert.equal(code, 0);
assert.equal(result.composition.metrics.properCrossings, 0);
assert.equal(result.composition.metrics.ambiguousCorridors, 0);
});
test('render output check: relationship labels cannot hide another shared-source route', () => {
for (const profile of ['standard', 'showcase']) {
const { code, result } = checkHtml(`label-route-${profile}`, `
<path data-edge-key="0" data-edge-id="approved" data-edge-from="dlq" data-edge-to="replay" data-composition-points="20,60;200,60" d="M 20 60 L 200 60" class="a-default" marker-end="url(#arrowhead)"/>
<path data-edge-key="1" data-edge-id="sample" data-edge-from="dlq" data-edge-to="ops" data-composition-points="70,55;150,55" d="M 70 55 L 150 55" class="a-dashed" marker-end="url(#arrowhead-dashed)"/>
<g data-detail="context" data-edge-key="0" data-edge-id="approved" data-edge-from="dlq" data-edge-to="replay" data-edge-label="approved replay">
<rect x="80" y="48" width="60" height="14" rx="3" class="c-mask"/>
<text x="110" y="58">approved replay</text>
</g>
`, profile);
assert.equal(result.composition.metrics.labelRouteClearanceIssues, 1);
assert.equal(result.composition.metrics.minLabelRouteClearance, 0);
const issue = result.composition.issues.find((item) => item.code === 'composition/label-route-clearance');
assert.deepEqual(issue.labelRelationship, { id: 'approved', from: 'dlq', to: 'replay', label: 'approved replay', collectionIndex: 0, artifactIndex: 1 });
assert.deepEqual(issue.otherRelationship, { id: 'sample', from: 'dlq', to: 'ops', label: '', collectionIndex: 1, artifactIndex: 2 });
assert.equal(issue.segmentIndex, 0);
assert.deepEqual(issue.labelRect, { x: 80, y: 48, width: 60, height: 14 });
assert.equal(issue.clearance, 0);
assert.equal(issue.intersectionLength, 60);
assert.equal(issue.threshold, profile === 'showcase' ? 4 : 2);
const check = result.checks.find((item) => item.name === 'label_route_clearance');
if (profile === 'standard') {
assert.equal(code, 0);
assert.equal(check.ok, true);
assert.equal(issue.severity, 'warning');
assert.deepEqual(result.composition.summary, { errors: 0, warnings: 1 });
} else {
assert.notEqual(code, 0);
assert.equal(check.ok, false);
assert.match(check.details[0], /approved.*sample/);
assert.match(check.details[0], /labelAt.*labelDx.*labelDy.*labelSegment/);
assert.equal(issue.severity, 'error');
assert.deepEqual(result.composition.summary, { errors: 1, warnings: 0 });
}
}
});
test('render output check: repeated endpoint messages keep their own stable owner identity', () => {
const { code, result } = checkHtml('sequence-repeated-endpoints', `
<g data-edge-key="0" data-edge-from="client" data-edge-to="api" data-edge-label="first request">
<path data-composition-edge-from="client" data-composition-edge-to="api" data-composition-points="20,20;200,20" d="M 20 20 L 200 20" class="a-default" marker-end="url(#arrowhead)"/>
<g data-detail="context"><rect x="70" y="2" width="80" height="16" class="c-mask"/></g>
</g>
<g data-edge-key="1" data-edge-from="client" data-edge-to="api" data-edge-label="second request">
<path data-composition-edge-from="client" data-composition-edge-to="api" data-composition-points="20,60;200,60" d="M 20 60 L 200 60" class="a-default" marker-end="url(#arrowhead)"/>
<g data-detail="context"><rect x="70" y="72" width="80" height="16" class="c-mask"/></g>
</g>
<path data-edge-key="2" data-edge-from="worker" data-edge-to="store" data-composition-points="20,80;200,80" d="M 20 80 L 200 80" class="a-dashed" marker-end="url(#arrowhead-dashed)"/>
`, 'showcase');
assert.notEqual(code, 0);
const issues = result.composition.issues.filter((item) => item.code === 'composition/label-route-clearance');
assert.equal(issues.length, 1);
assert.equal(issues[0].label, 'second request');
assert.equal(issues[0].labelRelationship.collectionIndex, 1);
assert.equal(issues[0].otherRelationship.collectionIndex, 2);
});
test('render output check: duplicate fragments of the owning relationship stay exempt', () => {
const { code, result } = checkHtml('label-owner-fragments', `
<path data-edge-key="0" data-edge-id="approved" data-edge-from="dlq" data-edge-to="replay" data-composition-points="20,60;200,60" d="M 20 60 L 200 60" class="a-default" marker-end="url(#arrowhead)"/>
<path data-edge-key="0" data-edge-id="approved" data-edge-from="dlq" data-edge-to="replay" data-composition-points="20,60;200,60" d="M 20 60 L 200 60" class="a-default" marker-end="url(#arrowhead)"/>
<g data-detail="context" data-edge-key="0" data-edge-id="approved" data-edge-from="dlq" data-edge-to="replay" data-edge-label="approved replay">
<rect x="80" y="48" width="60" height="14" rx="3" class="c-mask"/>
<text x="110" y="58">approved replay</text>
</g>
`, 'showcase');
assert.equal(code, 0);
assert.equal(result.composition.metrics.labelRouteClearanceIssues, 0);
assert.equal(result.composition.metrics.minLabelRouteClearance, null);
});
test('render output check: duplicate fragments of another relationship count once', () => {
const { result } = checkHtml('label-other-fragments', `
<path data-edge-key="0" data-edge-id="approved" data-edge-from="dlq" data-edge-to="replay" data-composition-points="20,60;200,60" d="M 20 60 L 200 60" class="a-default" marker-end="url(#arrowhead)"/>
<path data-edge-key="1" data-edge-id="sample" data-edge-from="dlq" data-edge-to="ops" data-composition-points="70,55;150,55" d="M 70 55 L 150 55" class="a-dashed" marker-end="url(#arrowhead-dashed)"/>
<path data-edge-key="1" data-edge-id="sample" data-edge-from="dlq" data-edge-to="ops" data-composition-points="70,55;150,55" d="M 70 55 L 150 55" class="a-dashed" marker-end="url(#arrowhead-dashed)"/>
<g data-detail="context" data-edge-key="0" data-edge-id="approved" data-edge-from="dlq" data-edge-to="replay" data-edge-label="approved replay">
<rect x="80" y="48" width="60" height="14" rx="3" class="c-mask"/>
<text x="110" y="58">approved replay</text>
</g>
`, 'showcase');
assert.equal(result.composition.metrics.labelRouteClearanceIssues, 1);
});
test('render output check: label-route thresholds include exact 2px and 4px boundaries', () => {
const body = (otherY) => `
<path data-edge-key="0" data-edge-from="a" data-edge-to="b" data-composition-points="20,70;200,70" d="M 20 70 L 200 70" class="a-default" marker-end="url(#arrowhead)"/>
<path data-edge-key="1" data-edge-from="c" data-edge-to="d" data-composition-points="70,${otherY};150,${otherY}" d="M 70 ${otherY} L 150 ${otherY}" class="a-dashed" marker-end="url(#arrowhead-dashed)"/>
<g data-detail="context" data-edge-key="0" data-edge-from="a" data-edge-to="b" data-edge-label="handoff">
<rect x="80" y="48" width="60" height="14" rx="3" class="c-mask"/>
<text x="110" y="58">handoff</text>
</g>
`;
const standardAtTwo = checkHtml('label-standard-two', body(64), 'standard');
assert.equal(standardAtTwo.code, 0);
assert.equal(standardAtTwo.result.composition.metrics.labelRouteClearanceIssues, 0);
assert.equal(standardAtTwo.result.composition.metrics.minLabelRouteClearance, 2);
const standardBelowTwo = checkHtml('label-standard-one-nine', body(63.9), 'standard');
assert.equal(standardBelowTwo.code, 0);
assert.equal(standardBelowTwo.result.composition.metrics.labelRouteClearanceIssues, 1);
assert.equal(standardBelowTwo.result.composition.summary.warnings, 1);
const showcaseAtTwo = checkHtml('label-showcase-two', body(64), 'showcase');
assert.notEqual(showcaseAtTwo.code, 0);
assert.equal(showcaseAtTwo.result.composition.metrics.labelRouteClearanceIssues, 1);
assert.equal(showcaseAtTwo.result.composition.issues.find((item) => item.code === 'composition/label-route-clearance').threshold, 4);
const showcaseAtFour = checkHtml('label-showcase-four', body(66), 'showcase');
assert.equal(showcaseAtFour.code, 0);
assert.equal(showcaseAtFour.result.composition.metrics.labelRouteClearanceIssues, 0);
assert.equal(showcaseAtFour.result.composition.metrics.minLabelRouteClearance, 4);
const showcaseBelowFour = checkHtml('label-showcase-three-nine', body(65.9), 'showcase');
assert.notEqual(showcaseBelowFour.code, 0);
assert.equal(showcaseBelowFour.result.composition.metrics.labelRouteClearanceIssues, 1);
assert.equal(showcaseBelowFour.result.composition.summary.errors, 1);
});
test('render output check: unrelated shared corridors warn in standard and fail showcase', () => {
for (const profile of ['standard', 'showcase']) {
const { code, result } = checkHtml(`corridor-${profile}`, `
<path data-edge-id="first" data-edge-from="a" data-edge-to="b" data-composition-points="20,60;140,60;140,100" d="M 20 60 L 140 60 L 140 100" class="a-default" marker-end="url(#arrowhead)"/>
<path data-edge-id="second" data-edge-from="c" data-edge-to="d" data-composition-points="60,60;180,60;180,100" d="M 60 60 L 180 60 L 180 100" class="a-dashed" marker-end="url(#arrowhead-dashed)"/>
`, profile);
assert.equal(result.composition.metrics.ambiguousCorridors, 1);
assert.equal(result.composition.issues[0].code, 'composition/ambiguous-corridor');
assert.equal(result.composition.issues[0].overlapLength, 80);
assert.deepEqual(result.composition.issues[0].from, [60, 60]);
assert.deepEqual(result.composition.issues[0].to, [140, 60]);
const check = result.checks.find((item) => item.name === 'relationship_corridors');
if (profile === 'standard') {
assert.equal(code, 0);
assert.equal(check.ok, true);
assert.deepEqual(result.composition.summary, { errors: 0, warnings: 1 });
assert.equal(result.composition.issues[0].severity, 'warning');
} else {
assert.notEqual(code, 0);
assert.equal(check.ok, false);
assert.match(check.details[0], /\[composition\/ambiguous-corridor\] showcase/);
assert.match(check.details[0], /relationship id "first".*relationship id "second"/);
assert.deepEqual(result.composition.summary, { errors: 1, warnings: 0 });
assert.equal(result.composition.issues[0].severity, 'error');
}
}
});
test('render output check: visible quadratic crossing is caught in showcase', () => {
const { code, result } = checkHtml('showcase-quadratic-crossing', `
<path data-edge-from="a" data-edge-to="b" d="M 20 90 Q 100 10 180 90" class="a-default" marker-end="url(#arrowhead)"/>
<path data-edge-from="c" data-edge-to="d" d="M 103 20 L 103 120" class="a-dashed" marker-end="url(#arrowhead-dashed)"/>
`, 'showcase');
assert.notEqual(code, 0);
assert.equal(result.composition.metrics.properCrossings, 1);
assert.equal(result.composition.status, 'fail');
});
test('render output check: container border runs fail both profiles with frame identity', () => {
for (const profile of ['standard', 'showcase']) {
const { code, result } = checkHtml(`border-run-${profile}`, `
<rect data-composition-frame-kind="stage" data-composition-frame-id="sources" x="40" y="40" width="160" height="80" rx="10"/>
<path data-edge-id="events" data-edge-from="web" data-edge-to="edge" data-composition-points="60,40;150,40" d="M 60 40 L 150 40" class="a-default" marker-end="url(#arrowhead)"/>
`, profile);
assert.notEqual(code, 0);
const check = result.checks.find((item) => item.name === 'container_border_runs');
assert.equal(check.ok, false);
assert.match(check.details[0], /\[composition\/container-border-run\].*relationship id "events"/);
assert.match(check.details[0], /stage "sources" top border for 90px/);
assert.equal(result.composition.summary.errors, 1);
assert.equal(result.composition.metrics.containerBorderRuns, 1);
assert.equal(result.composition.issues[0].code, 'composition/container-border-run');
}
});
test('render output check: perpendicular crossings, rounded-corner touches, and tangent Q curves pass', () => {
const { code, result } = checkHtml('border-run-exemptions', `
<rect data-composition-frame-kind="group" data-composition-frame-id="safe" x="40" y="40" width="160" height="80" rx="10"/>
<path data-edge-from="a" data-edge-to="b" d="M 100 10 L 100 80" class="a-default" marker-end="url(#arrowhead)"/>
<path data-edge-from="c" data-edge-to="d" d="M 40 40 L 49 40" class="a-default" marker-end="url(#arrowhead)"/>
<path data-edge-from="e" data-edge-to="f" d="M 20 70 Q 40 40 60 40" class="a-default" marker-end="url(#arrowhead)"/>
`, 'showcase');
assert.equal(code, 0);
assert.equal(result.composition.metrics.containerBorderRuns, 0);
});
test('render output check: a fully collinear quadratic primitive is a border run', () => {
const { code, result } = checkHtml('border-run-collinear-q', `
<rect data-composition-frame-kind="segment" data-composition-frame-id="retry" x="40" y="40" width="160" height="80" rx="10"/>
<path data-edge-from="a" data-edge-to="b" d="M 60 40 Q 100 40 140 40" class="a-default" marker-end="url(#arrowhead)"/>
`);
assert.notEqual(code, 0);
assert.equal(result.composition.metrics.containerBorderRuns, 1);
});
test('render output check: composition receipt records neutral normalized route metrics', () => {
const { code, result } = checkHtml('route-metrics', `
<path data-edge-from="a" data-edge-to="b" data-composition-points="0,20;10,20;30,20;30,28;50,28;50,50" d="M 0 20 L 30 20 L 30 28 L 50 28 L 50 50" class="a-default" marker-end="url(#arrowhead)"/>
`);
assert.equal(code, 0);
assert.equal(result.composition.metrics.maxBends, 3);
assert.equal(result.composition.metrics.routesOverSuggestedBends, 1);
assert.equal(result.composition.metrics.minSegmentPx, 8);
assert.equal(result.composition.metrics.shortSegmentCount, 1);
assert.equal(result.composition.metrics.shortInteriorSegmentCount, 1);
assert.equal(result.composition.metrics.shortEndpointSegmentCount, 0);
assert.equal(result.composition.metrics.microSegmentCount, 0);
assert.deepEqual(result.composition.suggestedLimits, { bendsPerRelationship: 2, stretch: 1.35, segmentPx: 16, microSegmentPx: 8 });
});
test('render output check: endpoint stubs from 8px pass while cramped interior turns are profile-aware', () => {
const clean = checkHtml('endpoint-stubs', `
<path data-edge-id="lane-hop" data-edge-from="a" data-edge-to="b" data-composition-points="0,20;13,20;13,60;80,60;80,73" d="M 0 20 L 13 20 L 13 60 L 80 60 L 80 73" class="a-default" marker-end="url(#arrowhead)"/>
`, 'showcase');
assert.equal(clean.code, 0);
assert.equal(clean.result.composition.metrics.shortEndpointSegmentCount, 2);
assert.equal(clean.result.composition.metrics.shortInteriorSegmentCount, 0);
const standard = checkHtml('short-turn-standard', `
<path data-edge-id="tight" data-edge-from="a" data-edge-to="b" data-composition-points="0,20;24,20;24,29;80,29" d="M 0 20 L 24 20 L 24 29 L 80 29" class="a-default" marker-end="url(#arrowhead)"/>
`, 'standard');
assert.equal(standard.code, 0);
assert.deepEqual(standard.result.composition.summary, { errors: 0, warnings: 1 });
assert.equal(standard.result.composition.issues[0].code, 'composition/short-interior-segment');
assert.equal(standard.result.checks.find((item) => item.name === 'route_rhythm').ok, true);
const showcase = checkHtml('short-turn-showcase', `
<path data-edge-id="tight" data-edge-from="a" data-edge-to="b" data-composition-points="0,20;24,20;24,29;80,29" d="M 0 20 L 24 20 L 24 29 L 80 29" class="a-default" marker-end="url(#arrowhead)"/>
`, 'showcase');
assert.notEqual(showcase.code, 0);
assert.deepEqual(showcase.result.composition.summary, { errors: 1, warnings: 0 });
const rhythm = showcase.result.checks.find((item) => item.name === 'route_rhythm');
assert.equal(rhythm.ok, false);
assert.match(rhythm.details[0], /\[composition\/short-interior-segment\] showcase relationship id "tight"/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,181 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(here, '..');
const cli = path.join(skillRoot, 'bin/archify.mjs');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-repair-receipt-'));
function run(args) {
return spawnSync(process.execPath, [cli, ...args], {
cwd: skillRoot,
encoding: 'utf8',
});
}
function writeFixture(name, source) {
const file = path.join(tmp, name);
fs.writeFileSync(file, JSON.stringify(source, null, 2));
return file;
}
function receipt(result) {
assert.doesNotThrow(() => JSON.parse(result.stdout), result.stdout || result.stderr);
return JSON.parse(result.stdout);
}
test('repair receipt: malformed JSON is one clean machine object without a Node stack', () => {
const input = path.join(tmp, 'malformed.workflow.json');
fs.writeFileSync(input, '{broken json');
const result = run(['validate', 'workflow', input, '--json']);
assert.equal(result.status, 1);
assert.equal(result.stderr, '');
const failure = receipt(result);
assert.equal(failure.schemaVersion, 1);
assert.equal(failure.ok, false);
assert.equal(failure.command, 'validate');
assert.equal(failure.stage, 'input');
assert.equal(failure.diagnostics.length, 1);
assert.deepEqual(failure.diagnostics[0].subject, { input });
assert.equal(failure.diagnostics[0].code, 'input/json-parse');
assert.equal(failure.diagnostics[0].severity, 'error');
assert.match(failure.diagnostics[0].evidence.reason, /JSON/);
assert.deepEqual(failure.diagnostics[0].supportedFixes, ['repair the JSON syntax and run validation again']);
assert.doesNotMatch(result.stdout, /\n\s+at\s|file:\/\//);
});
test('repair receipt: all five modes identify schema subjects and supported fixes', () => {
const cases = {
architecture: ['web-app.architecture.json', 'components'],
workflow: ['agent-tool-call.workflow.json', 'nodes'],
sequence: ['cache-miss-request.sequence.json', 'participants'],
dataflow: ['product-analytics.dataflow.json', 'nodes'],
lifecycle: ['agent-run.lifecycle.json', 'states'],
};
for (const [type, [example, collection]] of Object.entries(cases)) {
const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', example), 'utf8'));
source[collection][0].unexpected = true;
const identity = source[collection][0].id;
const input = writeFixture(`schema-${type}.json`, source);
const result = run(['validate', type, input, '--json']);
assert.equal(result.status, 1, `${type}: ${result.stderr || result.stdout}`);
assert.equal(result.stderr, '', type);
const failure = receipt(result);
const repair = failure.diagnostics.find((entry) => entry.code === 'schema/additionalProperties');
assert.ok(repair, type);
assert.deepEqual(repair.subject, {
diagramType: type,
path: `/${collection}/0`,
identity,
});
assert.equal(repair.evidence.additionalProperty, 'unexpected');
assert.deepEqual(repair.supportedFixes, ['remove unsupported property "unexpected"']);
}
});
test('repair receipt: human validation formats the same rule without a stack', () => {
const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples/agent-tool-call.workflow.json'), 'utf8'));
source.nodes[0].unexpected = true;
const input = writeFixture('human-schema.workflow.json', source);
const result = run(['validate', 'workflow', input]);
assert.equal(result.status, 1);
assert.equal(result.stdout, '');
assert.match(result.stderr, /\[schema\/additionalProperties\]/);
assert.match(result.stderr, /Fix: remove unsupported property "unexpected"/);
assert.doesNotMatch(result.stderr, /\n\s+at\s|file:\/\//);
});
test('repair receipt: validate and deliver share exact Clean Flow evidence while delivery preserves the trusted artifact', () => {
const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples/web-app.architecture.json'), 'utf8'));
source.connections[0] = {
...source.connections[0],
fromSide: 'right',
toSide: 'left',
via: [[100, 140], [220, 140]],
};
const input = writeFixture('blocked-route.architecture.json', source);
const output = path.join(tmp, 'trusted.html');
const trusted = '<!doctype html><title>trusted prior artifact</title>\n';
fs.writeFileSync(output, trusted);
const validated = run(['validate', 'architecture', input, '--quality', 'showcase', '--json']);
const delivered = run(['deliver', 'architecture', input, output, '--quality', 'showcase', '--json']);
assert.equal(validated.status, 1, validated.stderr || validated.stdout);
assert.equal(delivered.status, 1, delivered.stderr || delivered.stdout);
assert.equal(validated.stderr, '');
assert.equal(delivered.stderr, '');
const validateRepair = receipt(validated).diagnostics.find((entry) => entry.code === 'clean-flow/edge-through-node');
const deliverRepair = receipt(delivered).diagnostics.find((entry) => entry.code === 'clean-flow/edge-through-node');
assert.ok(validateRepair);
assert.ok(deliverRepair);
assert.deepEqual(deliverRepair, validateRepair);
assert.equal(validateRepair.subject.id, 'users-to-cdn');
assert.equal(validateRepair.evidence.obstacleId, 'auth');
assert.equal(validateRepair.evidence.segmentIndex, 0);
assert.equal(validateRepair.evidence.clearancePx, 2);
assert.ok(validateRepair.supportedFixes.some((fix) => fix.includes('route/via')));
assert.equal(fs.readFileSync(output, 'utf8'), trusted);
assert.deepEqual(fs.readdirSync(tmp).filter((name) => name.startsWith('.archify-delivery-')), []);
});
test('repair receipt: repository evidence failures retain a stable rule and exact repair', () => {
const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples/web-app.architecture.json'), 'utf8'));
source.meta.repository = {
url: 'https://github.com/example/repository',
revision: '0123456789abcdef0123456789abcdef01234567',
};
source.components[0].sources = [{ path: 'src/index.js', line: 1 }];
const input = writeFixture('evidence-root-required.architecture.json', source);
const result = run(['validate', 'architecture', input, '--json']);
assert.equal(result.status, 1);
assert.equal(result.stderr, '');
const repair = receipt(result).diagnostics[0];
assert.equal(repair.code, 'repository-evidence/root-required');
assert.deepEqual(repair.subject, { surface: 'repository-evidence', path: '/meta/repository' });
assert.deepEqual(repair.supportedFixes, ['pass --repo-root with the matching local Git checkout']);
});
test('repair receipt: public validate reports borderline desktop readability with a supported fix', () => {
const input = writeFixture('borderline-readability.architecture.json', {
schema_version: 1,
diagram_type: 'architecture',
meta: {
title: 'Borderline desktop readability',
quality_profile: 'showcase',
viewBox: [1826, 804],
},
components: [{
id: 'tool-runtime',
type: 'security',
label: 'ToolRuntime',
sublabel: 'OpenAI · Anthropic · provider gateways',
pos: [100, 100],
size: [194, 70],
}],
connections: [],
});
const result = run(['validate', 'architecture', input, '--quality', 'showcase', '--json']);
assert.equal(result.status, 1, result.stderr || result.stdout);
assert.equal(result.stderr, '');
const repair = receipt(result).diagnostics.find(
(entry) => entry.code === 'composition/desktop-readability',
);
assert.ok(repair);
assert.deepEqual(repair.subject, { check: 'composition' });
assert.ok(repair.evidence.projectedFontPx < repair.evidence.minimumProjectedFontPx);
assert.ok(repair.supportedFixes.some((fix) => fix.includes('reduce the viewBox width')));
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,217 @@
import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { startPreview } from '../bin/preview.mjs';
const here = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(here, '..');
const cli = path.join(skillRoot, 'bin', 'archify.mjs');
function git(repo, ...args) {
return execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
}
function fixture() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-evidence-repo-'));
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
fs.writeFileSync(path.join(root, 'src', 'router.js'), 'export function route(input) {\n return input.kind;\n}\n');
fs.writeFileSync(path.join(root, 'src', 'store.js'), 'export const store = new Map();\n');
git(root, 'init');
git(root, 'config', 'user.name', 'Archify Tests');
git(root, 'config', 'user.email', 'archify@example.test');
git(root, 'remote', 'add', 'origin', 'git@github.com:example/evidence-repo.git');
git(root, 'add', '.');
git(root, 'commit', '-m', 'fixture');
const revision = git(root, 'rev-parse', 'HEAD');
const diagram = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', 'web-app.architecture.json'), 'utf8'));
diagram.meta.repository = {
url: 'https://github.com/example/evidence-repo',
revision,
};
diagram.components[0].sources = [
{ path: 'src/router.js', line: 1, end_line: 3, label: 'Request router' },
{ path: 'src/store.js', line: 1 },
];
const input = path.join(root, 'diagram.architecture.json');
fs.writeFileSync(input, JSON.stringify(diagram, null, 2));
return { root, revision, diagram, input };
}
function run(args) {
return spawnSync(process.execPath, [cli, ...args], {
cwd: skillRoot,
encoding: 'utf8',
});
}
function evidencePayload(html) {
const match = html.match(/<script id="archify-source-evidence-data" type="application\/json">([\s\S]*?)<\/script>/);
assert.ok(match, 'verified evidence payload missing');
return JSON.parse(match[1]);
}
test('repository evidence accepts canonical HTTPS and common SSH remotes', () => {
const data = fixture();
const output = path.join(data.root, 'remote-form.html');
for (const remote of [
'https://github.com/example/evidence-repo.git/',
'git@github.com:example/evidence-repo.git',
'ssh://git@github.com/example/evidence-repo.git',
]) {
git(data.root, 'remote', 'set-url', 'origin', remote);
const result = run(['deliver', 'architecture', data.input, output, '--repo-root', data.root, '--json']);
assert.equal(result.status, 0, `${remote}: ${result.stderr || result.stdout}`);
}
});
async function waitForState(url, predicate, timeoutMs = 12000) {
const started = Date.now();
let latest;
while (Date.now() - started < timeoutMs) {
latest = await (await fetch(new URL('/state', url))).json();
if (predicate(latest)) return latest;
await new Promise((resolve) => setTimeout(resolve, 40));
}
assert.fail(`preview did not settle; latest state: ${JSON.stringify(latest)}`);
}
test('repository evidence is revision-verified, receipt-backed, searchable, and export-clean', () => {
const data = fixture();
const output = path.join(data.root, 'verified.html');
const result = run(['deliver', 'architecture', data.input, output, '--repo-root', data.root, '--json']);
assert.equal(result.status, 0, result.stderr || result.stdout);
const receipt = JSON.parse(result.stdout);
assert.deepEqual(receipt.evidence, {
verified: true,
repository: 'https://github.com/example/evidence-repo',
revision: data.revision,
references: 2,
});
const html = fs.readFileSync(output, 'utf8');
const evidence = evidencePayload(html);
assert.equal(evidence.verified, true);
assert.equal(evidence.repository.shortRevision, data.revision.slice(0, 7));
assert.equal(evidence.nodes.users.length, 2);
assert.equal(evidence.nodes.users[0].href, `https://github.com/example/evidence-repo/blob/${data.revision}/src/router.js#L1-L3`);
assert.match(html, /Verified source/);
assert.match(html, /Archify\.sourceEvidence = \(function \(\)/);
assert.match(html, /var sourceSearch = sources\.map/);
assert.match(html, /renderSourceEvidence\(id\)/);
assert.match(html, /referrerPolicy = 'no-referrer'/);
assert.match(html, /classList\.add\('source-evidence-beacon'\)/);
assert.match(html, /text\.textContent = viewerText\('viewer\.passport\.sourceMarker'\) \+ ' ' \+ count/);
assert.match(html, /Archify\.sourceEvidence\.installBeacons\(\)/);
assert.match(html, /querySelectorAll\('\[data-source-evidence-beacon\]'\)/);
assert.match(html, /data-source-evidence-original-label/);
const svg = html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
assert.doesNotMatch(svg, /src\/router\.js|github\.com\/example\/evidence-repo|source-evidence/);
});
test('repository evidence is opt-in and never appears in ordinary artifacts', () => {
const output = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'archify-no-evidence-')), 'plain.html');
const input = path.join(skillRoot, 'examples', 'web-app.architecture.json');
const result = run(['render', 'architecture', input, output]);
assert.equal(result.status, 0, result.stderr);
const html = fs.readFileSync(output, 'utf8');
assert.doesNotMatch(html, /id="archify-source-evidence-data"/);
assert.match(html, /id="focus-evidence" hidden/);
const svg = html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
assert.doesNotMatch(svg, /source-evidence-beacon|data-source-evidence-count/);
});
test('evidence fails closed without a root, on wrong origin, missing blobs, or impossible lines', () => {
const data = fixture();
const output = path.join(data.root, 'must-stay.html');
fs.writeFileSync(output, 'trusted previous artifact');
let result = run(['deliver', 'architecture', data.input, output, '--json']);
assert.equal(result.status, 1);
assert.equal(JSON.parse(result.stdout).stage, 'render');
assert.match(JSON.parse(result.stdout).error, /Pass --repo-root/);
assert.equal(fs.readFileSync(output, 'utf8'), 'trusted previous artifact');
git(data.root, 'remote', 'set-url', 'origin', 'https://github.com/example/other-repo.git');
result = run(['deliver', 'architecture', data.input, output, '--repo-root', data.root, '--json']);
assert.equal(result.status, 1);
assert.match(JSON.parse(result.stdout).error, /does not match/);
git(data.root, 'remote', 'set-url', 'origin', 'git@github.com:example/evidence-repo.git');
data.diagram.components[0].sources = [{ path: '../outside.js' }];
fs.writeFileSync(data.input, JSON.stringify(data.diagram));
result = run(['deliver', 'architecture', data.input, output, '--repo-root', data.root, '--json']);
assert.equal(result.status, 1);
assert.match(JSON.parse(result.stdout).error, /must stay inside the repository/);
data.diagram.components[0].sources = [{ path: 'src/router.js\n' }];
fs.writeFileSync(data.input, JSON.stringify(data.diagram));
result = run(['deliver', 'architecture', data.input, output, '--repo-root', data.root, '--json']);
assert.equal(result.status, 1);
assert.match(JSON.parse(result.stdout).error, /repo-relative POSIX path/);
data.diagram.components[0].sources = [{ path: 'src/missing.js' }];
fs.writeFileSync(data.input, JSON.stringify(data.diagram));
result = run(['deliver', 'architecture', data.input, output, '--repo-root', data.root, '--json']);
assert.equal(result.status, 1);
assert.match(JSON.parse(result.stdout).error, /does not identify a file/);
data.diagram.components[0].sources = [{ path: 'src/router.js', line: 99 }];
fs.writeFileSync(data.input, JSON.stringify(data.diagram));
result = run(['deliver', 'architecture', data.input, output, '--repo-root', data.root, '--json']);
assert.equal(result.status, 1);
assert.match(JSON.parse(result.stdout).error, /requests line 99/);
data.diagram.components[0].sources = [{ path: 'src/router.js', line: 4 }];
fs.writeFileSync(data.input, JSON.stringify(data.diagram));
result = run(['deliver', 'architecture', data.input, output, '--repo-root', data.root, '--json']);
assert.equal(result.status, 1);
assert.match(JSON.parse(result.stdout).error, /has 3 lines/);
assert.equal(fs.readFileSync(output, 'utf8'), 'trusted previous artifact');
});
test('--repo-root stays bounded to architecture and schema limits evidence shape', () => {
const data = fixture();
let result = run(['render', 'workflow', path.join(skillRoot, 'examples', 'agent-tool-call.workflow.json'), '--repo-root', data.root]);
assert.equal(result.status, 2);
assert.match(result.stderr, /architecture diagrams only/);
data.diagram.components[0].sources = [
{ path: 'src/router.js' },
{ path: 'src/router.js' },
{ path: 'src/router.js' },
{ path: 'src/router.js' },
];
fs.writeFileSync(data.input, JSON.stringify(data.diagram));
result = run(['validate', 'architecture', data.input, '--repo-root', data.root]);
assert.equal(result.status, 1);
assert.match(result.stderr, /must NOT have more than 3 items/);
});
test('live preview forwards repo-root and publishes only verified evidence', { timeout: 20000 }, async () => {
const data = fixture();
const output = path.join(data.root, 'preview.html');
const preview = await startPreview({
type: 'architecture',
input: data.input,
output,
repoRoot: data.root,
open: false,
debounceMs: 30,
pollMs: 60,
});
try {
const state = await waitForState(preview.url, (candidate) => candidate.status === 'verified');
assert.equal(state.revision, 1);
const html = await (await fetch(new URL('/artifact.html', preview.url))).text();
assert.equal(evidencePayload(html).repository.revision, data.revision);
} finally {
await preview.stop();
}
});
@@ -0,0 +1,52 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
function linguistGenerated(relativePath) {
const output = execFileSync(
'git',
['check-attr', 'linguist-generated', '--', relativePath],
{ cwd: repoRoot, encoding: 'utf8' },
).trim();
return output.slice(output.lastIndexOf(':') + 1).trim();
}
test('repository language metadata separates generated artifacts from implementation source', () => {
for (const generatedPath of [
'archify/examples/web-app-rendered.html',
'examples/web-app.html',
'docs/cases/mco-runtime.architecture.html',
'docs/gallery.html',
'docs/gallery/artifacts/web-app.architecture.html',
'docs/guide.html',
'docs/start.html',
'experiments/mco-showcase/mco-runtime.html',
'archify/renderers/shared/generated-brand-marks.mjs',
'archify/renderers/shared/generated-validators.mjs',
]) {
assert.equal(
linguistGenerated(generatedPath),
'true',
`${generatedPath} must be excluded from GitHub language statistics`,
);
}
for (const sourcePath of [
'archify/assets/template.html',
'scripts/gallery-template.html',
'scripts/guide-template.html',
'scripts/start-template.html',
'docs/index.html',
'archify/renderers/shared/geometry.mjs',
]) {
assert.equal(
linguistGenerated(sourcePath),
'unspecified',
`${sourcePath} must remain visible as implementation source`,
);
}
});
@@ -0,0 +1,123 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } 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 template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-route-journey-'));
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, example) {
const input = path.join(skillRoot, 'examples', example);
const output = path.join(tmp, `${mode}.html`);
const result = spawnSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
input,
output,
], { encoding: 'utf8' });
return { result, html: fs.existsSync(output) ? fs.readFileSync(output, 'utf8') : '' };
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all five renderers inherit native Route Journey controls outside canonical SVG', () => {
for (const [mode, example] of Object.entries(CASES)) {
const { result, html } = render(mode, example);
assert.equal(result.status, 0, result.stderr);
assert.match(html, /id="route-journey-controls" hidden role="group" aria-label="Route journey controls"/i, mode);
assert.match(html, /id="route-journey-prev"[^>]+aria-label="Previous route position"/i, mode);
assert.match(html, /id="route-journey-play"[^>]+aria-label="Play route journey"[^>]+aria-pressed="false"/i, mode);
assert.match(html, /id="route-journey-next"[^>]+aria-label="Next route position"/i, mode);
assert.match(html, /id="route-journey-overview"[^>]+aria-label="Show complete route overview"/i, mode);
assert.match(html, /document\.createElement\(options\.interactive === true \? 'button' : 'span'\)/, mode);
assert.doesNotMatch(canonicalSvg(html), /data-route-journey|route-journey-(?:flow|overlay)/, mode);
}
});
test('a position owns its exact ordered incoming edge while the full route remains authored truth', () => {
assert.match(template, /activeNodeIds = result\.nodes\.slice\(\)/);
assert.match(template, /activeEdges = result\.edges\.slice\(\)/);
assert.match(template, /activeEdges\.forEach\(function \(edge, step\) \{[\s\S]*?var destination = step \+ 1/);
assert.match(template, /destination === journeyIndex \? 'current' : 'future'/);
assert.match(template, /edge\.setAttribute\('data-route-journey-current', ''\)/);
assert.match(template, /journeyIndex > 0\) renderJourneyPulse\(activeEdges\[journeyIndex - 1\]\)/);
assert.match(template, /node\.setAttribute\('data-route-match', ''\)/);
assert.match(template, /edge\.setAttribute\('data-route-match', ''\)/);
assert.match(template, /svg\.setAttribute\('data-route-journey', \(journeyIndex \+ 1\) \+ '\/' \+ activeNodeIds\.length\)/);
assert.doesNotMatch(template, /renderJourneyPulse\([\s\S]{0,120}querySelector\(.*data-edge-from/);
});
test('route chips provide one roving tab stop, native activation, and manual ownership', () => {
assert.match(template, /item\.setAttribute\('data-route-journey-index', String\(index\)\)/);
assert.match(template, /item\.setAttribute\('tabindex', index === 0 \? '0' : '-1'\)/);
assert.match(template, /item\.setAttribute\('aria-label', viewerText\('viewer\.route\.position'/);
assert.match(template, /path\.addEventListener\('focusin'[\s\S]*?pauseJourney\(\{ preserveElapsed: true \}\)/);
assert.match(template, /path\.addEventListener\('keydown'[\s\S]*?event\.key === 'ArrowRight'/);
assert.match(template, /else if \(event\.key === 'Home'\) next = 0/);
assert.match(template, /else if \(event\.key === 'End'\) next = activeNodeIds\.length - 1/);
assert.match(template, /event\.key === 'Enter' \|\| event\.key === ' '/);
assert.match(template, /selectJourneyIndex\(Number\(button\.getAttribute\('data-route-journey-index'\)\)\)/);
assert.match(template, /button\.setAttribute\('aria-current', 'step'\)/);
});
test('playback is explicit, finite, resumable, and never leaks position into the route URL', () => {
assert.match(template, /var JOURNEY_DWELL_MS = 1100/);
assert.match(template, /journeyGeneration \+= 1/);
assert.match(template, /generation !== journeyGeneration \|\| !journeyPlaying/);
assert.match(template, /JOURNEY_DWELL_MS - journeyElapsedMs/);
assert.match(template, /preserveElapsed: options\.complete !== true/);
assert.match(template, /journeyIndex >= activeNodeIds\.length - 1[\s\S]*?pauseJourney\(\{ complete: true/);
assert.match(template, /applyJourneyState\(journeyIndex \+ 1[\s\S]*?journeyElapsedMs = 0;[\s\S]*?scheduleJourney\(\)/);
assert.doesNotMatch(template, /journeyTimer\s*=\s*(?:window\.)?setInterval/);
assert.match(template, /function playJourney\(\)[\s\S]*?if \(journeyIndex < 0\) applyJourneyState\(0/);
assert.match(template, /function syncFromHash\(\)[\s\S]*?choose\(parts\[1\], \{ updateUrl: false \}\)/);
assert.match(template, /'#route=' \+ encodeURIComponent\(startId\) \+ '~' \+ encodeURIComponent\(endId\)/);
assert.doesNotMatch(template, /#route=[^'\n]*journey/);
});
test('motion, camera, layered Escape, mobile, print, and embed boundaries stay explicit', () => {
assert.match(template, /Archify\.motionGovernor\.capable === true[\s\S]*?!Archify\.motionGovernor\.isPaused\(\)/);
assert.match(template, /Archify\.motionGovernor\.claim\('route'/);
assert.match(template, /reason: 'route-journey',[\s\S]*?maxScale: 1\.65,[\s\S]*?padding: 64,[\s\S]*?duration: 360/);
assert.match(template, /Archify\.routeProbe\.pauseJourney\(\{ preserveElapsed: true, reason: reason \|\| 'manual' \}\)/);
assert.match(template, /event\.target\.closest\('\.diagram-nav, \.focus-chip, \.node-finder, \.diagram-guide, \.overview-map, \.route-probe, \.semantic-lens'\)/);
assert.match(template, /reason: 'guide'/);
assert.match(template, /window\.addEventListener\('beforeprint'[\s\S]*?pauseJourney/);
assert.match(template, /function escapeRoute\(options\)[\s\S]*?return 'paused'[\s\S]*?return 'overview'[\s\S]*?return 'cleared'/);
assert.match(template, /Archify\.routeProbe\.escape\(\{ restoreFocus: true \}\)/);
assert.match(template, /\.route-probe\[data-route-dock="top"\] \{\s*top: 1rem;\s*bottom: auto;/);
assert.match(template, /function updateDocking\(\) \{\s*if \(panel\.hidden\)/);
assert.doesNotMatch(template, /if \(panel\.hidden \|\| window\.innerWidth > 720\)/);
assert.match(template, /@media \(max-width: 720px\)[\s\S]*?\.route-probe-node \{ min-height: 2\.75rem !important; \}/);
assert.match(template, /\.route-journey-controls button \{ min-height: 2\.75rem; \}/);
assert.match(template, /@media print[\s\S]*?\.route-probe-overlay, \.route-journey-overlay \{ display: none !important; \}/);
assert.match(template, /svg\[data-route-active\] \[data-node-id\],[\s\S]*?filter: none !important/);
assert.match(template, /html\[data-embed="true"\][\s\S]*?\.route-probe/);
assert.match(template, /html\[data-motion="still"\] \.route-journey-flow/);
assert.match(template, /@media \(prefers-reduced-motion: reduce\)[\s\S]*?\.route-journey-overlay/);
});
test('standalone export strips every journey attribute and transient pulse', () => {
assert.match(template, /clone\.removeAttribute\('data-route-journey'\)/);
assert.match(template, /clone\.querySelectorAll\('\[data-route-journey-overlay\]'\)/);
assert.match(template, /el\.removeAttribute\('data-route-journey-state'\)/);
assert.match(template, /el\.removeAttribute\('data-route-journey-current'\)/);
assert.match(template, /!clone\.hasAttribute\('data-route-journey'\)/);
assert.match(template, /canonicalStateClean[\s\S]*?\[data-route-journey-overlay\][\s\S]*?\[data-route-journey-current\]/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,114 @@
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-route-probe-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all typed renderers inherit one viewer-only Route Probe', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /id="route-probe" hidden role="region" aria-labelledby="route-probe-title"/, mode);
assert.match(html, /id="btn-route-probe"[^>]+aria-label="Trace a directed route"[^>]+aria-pressed="false"[^>]+aria-controls="route-probe"/, mode);
assert.match(html, /Archify\.routeProbe = \(function \(\)/, mode);
assert.match(html, /Route Probe — shortest directed path over compiled semantics/, mode);
assert.equal((html.match(/<svg\b/g) || []).length, 1, `${mode} keeps one static canonical SVG`);
assert.doesNotMatch(canonicalSvg(html), /data-route-|route-probe-flow/, mode);
}
});
test('Route Probe uses deterministic authored-direction BFS and exposes reachability first', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /function outgoingByNode\(\)/);
assert.match(html, /var from = edge\.getAttribute\('data-edge-from'\)/);
assert.match(html, /var to = edge\.getAttribute\('data-edge-to'\)/);
assert.match(html, /if \(!byId\[from\] \|\| !byId\[to\] \|\| from === to\) return/);
assert.match(html, /outgoing\[from\]\.push\(\{ edge: edge, to: to \}\)/);
assert.match(html, /function reachableFrom\(source\)/);
assert.match(html, /data-route-candidate/);
assert.match(html, /function shortestDirectedPath\(source, target\)/);
assert.match(html, /var queue = \[source\]/);
assert.match(html, /previous\[link\.to\] = \{ from: queue\[cursor\], edge: link\.edge \}/);
assert.match(html, /routeEdges\.unshift\(step\.edge\)/);
assert.match(html, /nodeIds\.unshift\(step\.from\)/);
});
test('Route Probe turns a two-node question into a readable route receipt and stable link', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /svg\.setAttribute\('data-route-picking', 'target'\)/);
assert.match(html, /svg\.setAttribute\('data-route-active', startId \+ '~' \+ endId\)/);
assert.match(html, /node\.setAttribute\('data-route-step', String\(step\)\)/);
assert.match(html, /edge\.setAttribute\('data-route-match', ''\)/);
assert.match(html, /clone\.setAttribute\('pathLength', '1'\)/);
assert.match(html, /clone\.style\.setProperty\('--route-step', String\(step\)\)/);
assert.match(html, /#route=' \+ encodeURIComponent\(startId\) \+ '~' \+ encodeURIComponent\(endId\)/);
assert.match(html, /new URLSearchParams\(location\.hash\.replace/);
assert.match(html, /Archify\.view\.reveal\(result\.nodes, \{ includeNeighbors: false, reason: 'route' \}\)/);
assert.match(html, /shortest authored route/);
});
test('Route Probe hands large-diagram endpoint selection to a reachability-aware Finder', () => {
const html = render('dataflow', CASES.dataflow);
assert.match(html, /id="route-probe-find"[^>]+aria-label="Find a route start"[^>]+data-node-finder-trigger/);
assert.match(html, /function hopDistancesFrom\(source\)/);
assert.match(html, /kind: 'route-source'/);
assert.match(html, /outgoing\[id\] && outgoing\[id\]\.length/);
assert.match(html, /kind: 'route-target'/);
assert.match(html, /Object\.keys\(distances\)\.filter/);
assert.match(html, /targetBadges\[id\] = viewerCount\('viewer\.route\.hop', distances\[id\]\)/);
assert.match(html, /Archify\.finder\.open\(\{ context: context \}\)/);
assert.match(html, /findBtn\.textContent = viewerText\('viewer\.route\.destination\.find'\)/);
assert.match(html, /panel\.setAttribute\('data-finder-open', 'true'\)/);
assert.match(html, /\.route-probe\[data-finder-open="true"\]/);
});
test('Route Probe keeps pointer, keyboard, motion, embed, and export boundaries explicit', () => {
const html = render('sequence', CASES.sequence);
assert.match(html, /svg\.addEventListener\('click', interceptSelection, true\)/);
assert.match(html, /svg\.addEventListener\('keydown', interceptSelection, true\)/);
assert.match(html, /event\.key !== 'Enter' && event\.key !== ' '/);
assert.match(html, /e\.key === 'r' \|\| e\.key === 'R'/);
assert.match(html, /e\.key === 'Escape' && Archify\.routeProbe\.active\(\)/);
assert.match(html, /html\[data-embed="true"\] \.route-probe/);
assert.match(html, /html\.getAttribute\('data-embed'\) === 'true'/);
assert.match(html, /\.route-probe\[data-route-dock="top"\]/);
assert.match(html, /function overlapArea\(a, b\)/);
assert.match(html, /score\(topCandidate\) <= score\(bottomCandidate\)/);
assert.match(html, /container\.addEventListener\('scroll', updateDocking, \{ passive: true \}\)/);
assert.match(html, /@keyframes archify-route-probe-flow/);
assert.match(html, /@media \(prefers-reduced-motion: reduce\)[\s\S]+\.route-probe-flow \{[\s\S]+animation: none !important/);
assert.match(html, /clone\.removeAttribute\('data-route-picking'\)/);
assert.match(html, /clone\.removeAttribute\('data-route-active'\)/);
assert.match(html, /clone\.querySelectorAll\('\[data-route-probe-overlay\]'\)/);
assert.match(html, /!clone\.hasAttribute\('data-route-active'\)/);
assert.doesNotMatch(canonicalSvg(html), /data-route-|route-probe-flow/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,163 @@
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 repoRoot = path.resolve(skillRoot, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-route-share-card-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all five renderers inherit one resolved-only Route Share Card Export item', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /data-action="route-share-card"[^>]*hidden disabled[^>]*>[\s\S]*?Route Share Card[\s\S]*?1200(?:&times;|×)630 PNG/, mode);
assert.match(html, /function syncRouteShareItem\(\)/, mode);
assert.match(html, /routeShareItem\.hidden = !snapshot;/, mode);
assert.match(html, /routeShareItem\.disabled = !snapshot;/, mode);
assert.match(html, /\.toolbar \.export-menu button\[hidden\] \{ display: none; \}/, mode);
assert.doesNotMatch(html, /id="route-probe-share"|class="route-probe-share"/, mode);
assert.doesNotMatch(canonicalSvg(html), /data-share-route(?:-|=)/, mode);
}
});
test('Route Share Card menu lifecycle excludes hidden state from keyboard navigation', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /function open\(focusLast\)[\s\S]*?syncRouteShareItem\(\);/);
assert.match(html, /items\(\)\.filter\(function \(i\) \{ return !i\.hidden && !i\.disabled; \}\)/);
assert.match(html, /function clear\(options\)[\s\S]*?Archify\.exportMenu\.syncRouteShare\(\)/);
assert.match(html, /function showResult\(result, options\)[\s\S]*?Archify\.exportMenu\.syncRouteShare\(\)/);
assert.match(html, /html\[data-embed="true"\] \.toolbar/);
assert.match(html, /@media print[\s\S]*?\.toolbar/);
});
test('Route Share Card snapshots copy exact resolved node and relationship identity without rerouting', () => {
const html = render('architecture', CASES.architecture);
const snapshotBlock = html.match(/function exportSnapshot\(\) \{[\s\S]*?\n \}/)?.[0] || '';
const geometryBlock = html.match(/function hasDrawableGeometry\(element\) \{[\s\S]*?\n \}/)?.[0] || '';
assert.match(snapshotBlock, /nodeIds: activeNodeIds\.slice\(\)/);
assert.match(snapshotBlock, /hops: activeEdges\.length/);
assert.match(snapshotBlock, /edges: activeEdges\.map\(function \(edge\) \{/);
assert.match(snapshotBlock, /key: edge\.getAttribute\('data-edge-key'\)/);
assert.match(snapshotBlock, /seenNodeIds = Object\.create\(null\)/);
assert.match(snapshotBlock, /seenEdgeKeys = Object\.create\(null\)/);
assert.match(snapshotBlock, /fragment\.getAttribute\('data-edge-from'\) === activeNodeIds\[index\]/);
assert.match(snapshotBlock, /drawableFragments = fragments\.filter\(hasDrawableGeometry\)/);
assert.match(snapshotBlock, /drawableFragments\.length !== 1/);
assert.match(snapshotBlock, /drawableFragments\[0\] !== edge/);
assert.match(geometryBlock, /geometry\.getTotalLength/);
assert.match(geometryBlock, /Number\.isFinite\(length\) && length > 0/);
assert.match(geometryBlock, /nan\|infinity/i);
assert.match(html, /exportSnapshot: exportSnapshot/);
assert.doesNotMatch(snapshotBlock, /shortestDirectedPath|outgoingByNode|reachableFrom|labelAt|nearest/i);
});
test('Route Share Card snapshot fails closed when the resolved DOM becomes stale or conflicting', () => {
const html = render('workflow', CASES.workflow);
const snapshotBlock = html.match(/function exportSnapshot\(\) \{[\s\S]*?\n \}/)?.[0] || '';
assert.match(snapshotBlock, /activeNodeIds\.length < 2/);
assert.match(snapshotBlock, /activeEdges\.length !== activeNodeIds\.length - 1/);
assert.match(snapshotBlock, /allNodes\.filter\(function \(node\)/);
assert.match(snapshotBlock, /!edge \|\| !svg\.contains\(edge\)/);
assert.match(snapshotBlock, /!edgeKey \|\| seenEdgeKeys\[edgeKey\]/);
assert.match(snapshotBlock, /!fragments\.every/);
assert.match(snapshotBlock, /return null/);
});
test('Route variant decorates only a finite canonical clone with dedicated static attributes', () => {
const html = render('architecture', CASES.architecture);
const applyBlock = html.match(/function applyRouteSnapshot\(clone, snapshot\) \{[\s\S]*?\n \}/)?.[0] || '';
assert.match(applyBlock, /snapshot\.edges\.length !== snapshot\.nodeIds\.length - 1/);
assert.match(applyBlock, /snapshot\.hops !== snapshot\.edges\.length/);
assert.match(applyBlock, /matchedNodes\.length !== 1/);
assert.match(applyBlock, /matchedEdges\.every/);
assert.match(applyBlock, /drawableMatches = matchedEdges\.filter\(hasDrawableGeometry\)/);
assert.match(applyBlock, /drawableMatches\.length !== 1/);
assert.match(applyBlock, /data-share-route-match/);
assert.match(applyBlock, /data-share-route-start/);
assert.match(applyBlock, /data-share-route-middle/);
assert.match(applyBlock, /data-share-route-end/);
assert.match(applyBlock, /clone\.removeAttribute\('data-animation'\)/);
assert.doesNotMatch(applyBlock, /setAttribute\('data-route-(?:match|step|start|end|active|journey)/);
assert.match(html, /canonicalStateClean && finiteSvgDimensions && applyRouteSnapshot\(clone, opts\.routeSnapshot\)/);
assert.match(html, /Number\.isFinite\(vb\.width\)[\s\S]*?vb\.width > 0 && vb\.height > 0/);
assert.ok(html.indexOf('var canonicalStateClean =') < html.indexOf('applyRouteSnapshot(clone, opts.routeSnapshot)'), 'canonical cleanup must precede route decoration');
});
test('clone-only Route styling retains context and distinguishes start, middle, and end without motion', () => {
const html = render('dataflow', CASES.dataflow);
assert.match(html, /svg\[data-share-route\] \[data-node-id\], svg\[data-share-route\] \[data-edge-from\] \{ opacity: 0\.18; \}/);
assert.match(html, /svg\[data-share-route\] \[data-share-route-match\] \{ opacity: 1; \}/);
assert.match(html, /data-share-route-start[\s\S]*?stroke-dasharray: 5 3/);
assert.match(html, /data-share-route-middle[\s\S]*?stroke-width: 2\.2/);
assert.match(html, /data-share-route-end[\s\S]*?stroke-width: 3\.4/);
assert.doesNotMatch(html.match(/if \(opts\.routeSnapshot\) \{[\s\S]*?\n \}/)?.[0] || '', /display:\s*none|animation:|filter:|transform:/);
});
test('Route Share Card reuses one 1200x630 variant seam and publishes a truthful receipt', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /function rasterizeShareCard\(options\)/);
assert.match(html, /options\.variant !== 'route'/);
assert.match(html, /var snapshot = Archify\.routeProbe && Archify\.routeProbe\.exportSnapshot\(\)/);
assert.match(html, /renderShareCard\(\{ routeSnapshot: snapshot \}\)/);
assert.doesNotMatch(html, /function rasterizeRouteShareCard|routeShareCard:/);
assert.match(html, /var title = titleNode \? titleNode\.textContent : document\.title;/);
assert.match(html, /viewerCount\('viewer\.export\.card\.routeSummary', routeSnapshot\.hops/);
assert.match(html, /source: routeSnapshot\.source\.label/);
assert.match(html, /target: routeSnapshot\.target\.label/);
assert.match(html, /recordExportReceipt\('share-card', blob, false, \{ width: SHARE_CARD_WIDTH, height: SHARE_CARD_HEIGHT \}, 'route', true\)/);
assert.match(html, /diagramFilename\(\) \+ '-route-share-card\.png'/);
assert.match(html, /data-last-export-variant/);
assert.match(html, /data-last-export-route-state-clean/);
assert.match(html, /clearExportReceipt\(\);[\s\S]*?var snapshot = Archify\.routeProbe/);
assert.match(html, /function runExport\(format\)[\s\S]*?clearExportReceipt\(\);/);
assert.match(html, /var ctx = canvas2dOrThrow\(canvas, viewerText\('viewer\.export\.shareCard'\)\)/);
});
test('skill and READMEs describe the optional Export variant and show one real card without changing the hero', () => {
const viewer = fs.readFileSync(path.join(skillRoot, 'references', 'viewer-runtime.md'), 'utf8');
assert.match(viewer, /Export → Route Share Card/);
assert.match(viewer, /format=share-card/);
assert.match(viewer, /variant=route/);
assert.match(viewer, /data-share-route-\*/);
assert.match(viewer, /download-only/i);
for (const readme of ['README.md', 'README_EN.md', 'README_ZH.md']) {
const text = fs.readFileSync(path.join(repoRoot, readme), 'utf8');
assert.match(text, /Export → Route Share Card/, readme);
assert.match(text, /docs\/assets\/archify-route-share-card\.png/, readme);
}
const png = fs.readFileSync(path.join(repoRoot, 'docs/assets/archify-route-share-card.png'));
assert.equal(png.subarray(0, 8).toString('hex'), '89504e470d0a1a0a');
assert.equal(png.readUInt32BE(16), 1200);
assert.equal(png.readUInt32BE(20), 630);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,88 @@
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-semantic-camera-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function svg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all typed renderers ship the same geometry-neutral semantic camera', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /function frameDesktop\(ids, options\)/, mode);
assert.match(html, /function semanticIds\(ids, includeNeighbors\)/, mode);
assert.match(html, /if \(seeds\[from\] \|\| seeds\[to\]\) \{ wanted\[from\] = true; wanted\[to\] = true; \}/, mode);
assert.match(html, /contentScale = Math\.min\(svgWidth \/ viewBox\.width, svgHeight \/ viewBox\.height\)/, mode);
assert.match(html, /targetScale = Math\.max\(1, Math\.min\(maxScale, targetScale\)\)/, mode);
assert.match(html, /visibleTop = Math\.max\(0, -containerRect\.top\)/, mode);
assert.match(html, /visibleBottom - visibleTop >= 240/, mode);
assert.match(html, /data-camera-mode/, mode);
assert.match(html, /data-camera-indicator/, mode);
assert.match(html, /var resolvedLevel = semantic \? viewerText\('viewer\.nav\.level\.auto'\) : levelLabel/, mode);
assert.match(html, /is-camera-moving/, mode);
assert.match(html, /cubic-bezier\(0\.22, 1, 0\.36, 1\)/, mode);
assert.doesNotMatch(svg(html), /data-camera-mode|is-camera-moving|AUTO /, mode);
}
});
test('semantic camera follows reader intent but yields to manual navigation', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /beginHandoff\(previousIndex, index, previous, view, outgoingBeatIndex, options\.playback === true \? 'playback' : 'guided'\)/);
assert.match(html, /reveal\(\[id\], \{ includeNeighbors: true, reason: 'focus' \}\)/);
assert.match(html, /reveal\(\[id\], \{ includeNeighbors: true, reason: 'relationship' \}\)/);
assert.match(html, /reveal\(\[id\], \{ includeNeighbors: true, reason: 'finder' \}\)/);
assert.match(html, /function interruptCamera\(reason\)/);
assert.match(html, /Archify\.guidedViews\.pause\(\)/);
assert.match(html, /container\.addEventListener\('pointerdown',[\s\S]+interruptCamera\(\)/);
assert.match(html, /\.overview-map, \.route-probe, \.semantic-lens/);
assert.match(html, /window\.innerWidth <= 720 && container\.hasAttribute\('data-wide-diagram'\) && Date\.now\(\) > autoScrollUntil/);
assert.match(html, /reset\(\{ automatic: true \}\)/);
assert.match(html, /routeReceipt\.hasAttribute\('data-route-journey'\)/);
assert.match(html, /receiptBottom \+ 24/);
});
test('semantic camera keeps mobile on its contained scroll model and respects reduced motion', () => {
const html = render('sequence', CASES.sequence);
assert.match(html, /if \(window\.innerWidth > 720\) return frameDesktop\(ids, options\)/);
assert.match(html, /if \(!container\.hasAttribute\('data-wide-diagram'\)\) \{[\s\S]+cameraReceipt\(\{ scale: 1, x: 0, y: 0, mode: 'semantic' \}/);
assert.match(html, /state\.scale = 1;[\s\S]+state\.x = 0;[\s\S]+state\.y = 0;[\s\S]+state\.mode = 'semantic';[\s\S]+apply\(\)/);
assert.match(html, /autoScrollUntil = Date\.now\(\) \+ \(instant \? 50 : 470\)/);
assert.match(html, /behavior: instant \? 'auto' : 'smooth'/);
assert.match(html, /svg \[data-node-id\], svg \[data-edge-from\], svg \[data-detail\], svg \[data-detail-anchor\], svg \[data-legend-hit\], svg \{ transition: none !important; \}/);
});
test('semantic camera remains outside canonical SVG export state', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /clone\.style\.removeProperty\('transform'\)/);
assert.match(html, /clone\.removeAttribute\('data-view-scale'\)/);
assert.match(html, /!clone\.style\.getPropertyValue\('transform'\)/);
assert.doesNotMatch(svg(html), /style="[^"]*transform|data-view-scale|data-camera-mode/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,90 @@
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-semantic-flow-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all typed renderers inherit one selection-triggered Semantic Flow signal', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /function renderFlowOverlay\(entries\)/, mode);
assert.match(html, /var MAX_LENS_FLOW_EDGES = 24/, mode);
assert.match(html, /setAttribute\('class', 'semantic-lens-flow'\)/, mode);
assert.match(html, /data-semantic-lens-overlay/, mode);
assert.doesNotMatch(canonicalSvg(html), /semantic-lens-flow|semantic-lens-overlay|data-lens-flow/, mode);
}
});
test('Semantic Flow clones only exact matched authored geometry and preserves direction', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /var matchedFlow = \[\]/);
assert.match(html, /direction = fromKind === selectedKinds\[0\] \? 'forward' : 'reverse'/);
assert.match(html, /fromKind === selectedKinds\[0\] && toKind !== selectedKinds\[0\] \? 'out'/);
assert.match(html, /toKind === selectedKinds\[0\] && fromKind !== selectedKinds\[0\] \? 'in'/);
assert.match(html, /matchedFlow\.push\(\{ edge: edge, direction: direction \}\)/);
assert.match(html, /clone\.removeAttribute\('marker-end'\)/);
assert.match(html, /clone\.removeAttribute\('data-edge-key'\)/);
assert.match(html, /clone\.setAttribute\('pathLength', '1'\)/);
assert.match(html, /wrapper\.setAttribute\('transform', entry\.edge\.members\[0\]\.getAttribute\('transform'\)\)/);
assert.match(html, /svg\.setAttribute\('data-lens-flow-count', String\(entries\.length\)\)/);
assert.match(html, /svg\.insertBefore\(overlay, firstNode\)/);
});
test('Semantic Flow has preset identities and motion-safe density boundaries', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /\.semantic-lens-flow\[data-direction="out"\],[\s\S]+var\(--frontend-stroke\)/);
assert.match(html, /\.semantic-lens-flow\[data-direction="in"\],[\s\S]+var\(--database-stroke\)/);
assert.match(html, /\.semantic-lens-flow\[data-direction="within"\][\s\S]+var\(--messagebus-stroke\)/);
assert.match(html, /svg\[data-preset="signal-flow"\] \.semantic-lens-flow/);
assert.match(html, /svg\[data-preset="blueprint"\] \.semantic-lens-flow/);
assert.match(html, /@keyframes archify-semantic-lens-flow/);
assert.match(html, /animation: archify-semantic-lens-flow 1\.35s linear 1 both/);
assert.match(html, /entries\.length > MAX_LENS_FLOW_EDGES/);
assert.match(html, /data-lens-flow-density', 'quiet'/);
assert.match(html, /html\[data-embed="true"\] \.semantic-lens-overlay/);
assert.match(html, /@media print \{[\s\S]+\.semantic-lens-overlay \{ display: none !important; \}/);
assert.match(html, /@media \(prefers-reduced-motion: reduce\)[\s\S]+\.semantic-lens-flow \{[\s\S]+animation: none !important/);
});
test('Semantic Flow cleanup and exports remain canonical', () => {
const html = render('dataflow', CASES.dataflow);
assert.match(html, /function removeFlowOverlay\(\)/);
assert.match(html, /svg\.querySelectorAll\('\[data-semantic-lens-overlay\]'\)/);
assert.match(html, /svg\.removeAttribute\('data-lens-flow-count'\)/);
assert.match(html, /svg\.removeAttribute\('data-lens-flow-density'\)/);
assert.match(html, /clone\.removeAttribute\('data-lens-flow-count'\)/);
assert.match(html, /clone\.removeAttribute\('data-lens-flow-density'\)/);
assert.match(html, /clone\.querySelectorAll\('\[data-semantic-lens-overlay\]'\)/);
assert.match(html, /\[data-semantic-lens-overlay\],[^']*\[data-lens-match\]/);
assert.doesNotMatch(canonicalSvg(html), /semantic-lens-flow|semantic-lens-overlay|data-lens-flow/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,156 @@
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-semantic-legend-'));
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, mutate) {
const output = path.join(tmp, `${mode}.html`);
let input = path.join(skillRoot, 'examples', CASES[mode]);
if (mutate) {
const document = JSON.parse(fs.readFileSync(input, 'utf8'));
mutate(document);
input = path.join(tmp, `${mode}.json`);
fs.writeFileSync(input, JSON.stringify(document));
}
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
input,
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
function values(svg, attribute) {
const pattern = new RegExp(`${attribute}="([^"]+)"`, 'g');
return [...svg.matchAll(pattern)].map((match) => match[1]);
}
test('only legends with an exact node-kind meaning publish bridge entries', () => {
const architecture = canonicalSvg(render('architecture'));
const architectureKinds = new Set(values(architecture, 'data-node-kind'));
assert.deepEqual(new Set(values(architecture, 'data-legend-kind')), architectureKinds);
assert.equal((architecture.match(/data-legend-bridge=""/g) || []).length, 1);
const workflow = canonicalSvg(render('workflow', (document) => {
// The production fixture intentionally fills its legend band with authored
// routes, so legacy implicit-auto correctly hides that legend. Keep this
// semantic bridge contract focused by rendering the same typed nodes with
// an explicit full legend and no relationship geometry in the band.
document.meta.legend = { mode: 'all' };
delete document.meta.viewBox;
document.edges = [];
delete document.mainPath;
}));
assert.deepEqual(values(workflow, 'data-legend-kind'), [
'frontend', 'backend', 'security', 'messagebus', 'database', 'cloud', 'external',
]);
const lifecycle = canonicalSvg(render('lifecycle'));
assert.deepEqual(values(lifecycle, 'data-legend-kind'), [
'start', 'active', 'waiting', 'decision', 'success', 'failure',
]);
const sequence = canonicalSvg(render('sequence'));
assert.deepEqual(values(sequence, 'data-legend-semantic-kind'), [
'emphasis', 'return', 'security', 'dashed', 'default',
]);
assert.doesNotMatch(sequence, /data-legend-bridge|data-legend-kind=/);
assert.match(sequence, />Legend</);
const dataflow = canonicalSvg(render('dataflow'));
assert.deepEqual(values(dataflow, 'data-legend-semantic-kind'), [
'emphasis', 'security', 'dashed', 'database', 'default',
]);
assert.deepEqual(values(dataflow, 'data-legend-kind'), ['database']);
assert.equal((dataflow.match(/data-legend-bridge=""/g) || []).length, 1);
assert.ok(values(dataflow, 'data-node-kind').includes('database'));
assert.match(dataflow, />Legend</);
});
test('runtime decoration derives counts from compiled node facts and stays viewer-only', () => {
const html = render('architecture');
const svg = canonicalSvg(html);
assert.match(html, /collectKinds\(\)\.forEach\(function \(kind\) \{ facts\[kind\.id\] = kind; \}\)/);
assert.match(html, /var count = fact \? fact\.nodes\.length : 0/);
assert.match(html, /data-legend-bridge-runtime/);
assert.match(html, /data-legend-count-badge/);
assert.match(html, /entry\.setAttribute\('role', 'button'\)/);
assert.match(html, /legendBridge\.setAttribute\('role', legendEntries\.length >= 3 \? 'toolbar' : 'group'\)/);
assert.match(html, /var visibleLabel = entry\.getAttribute\('data-legend-label'\) \|\| fact\.label/);
assert.match(html, /entry\.setAttribute\('aria-label', viewerCount\('viewer\.lens\.legend\.inspect', count/);
assert.match(html, /if \(!legendBridge \|\| html\.getAttribute\('data-embed'\) === 'true'\) return false/);
assert.doesNotMatch(svg, /data-legend-bridge-runtime|data-legend-count=|role="toolbar"/);
assert.doesNotMatch(svg, /data-legend-kind="[^"]+"[^>]+(?:role=|aria-pressed=)/);
});
test('preview is soft, input-aware, and yields to stronger exploration owners', () => {
const html = render('workflow');
const preview = html.slice(
html.indexOf('function previewLegendKind'),
html.indexOf('function syncLegendPreview'),
);
assert.match(html, /window\.matchMedia\('\(hover: hover\) and \(pointer: fine\)'\)/);
assert.match(html, /event\.pointerType === 'touch' \|\| \(finePointerQuery && !finePointerQuery\.matches\)/);
assert.match(html, /legendBridge\.addEventListener\('focusin'/);
assert.match(html, /legendBridge\.addEventListener\('focusout'/);
assert.match(preview, /data-legend-preview-match/);
assert.match(preview, /data-legend-preview-peer/);
assert.doesNotMatch(preview, /renderFlowOverlay|data-semantic-lens-overlay/);
assert.match(html, /selectedKinds\.length > 0 \|\| !panel\.hidden \|\| html\.getAttribute\('data-present'\) === 'true'/);
assert.match(html, /data-focus-active.*data-intent-trace-active/s);
assert.match(html, /data-route-picking.*data-story-active.*data-relationship-preview-active/s);
assert.match(html, /svg\[data-legend-preview-active\] \[data-node-id\]/);
});
test('activation delegates to Semantic Lens and supports roving keyboard navigation', () => {
const html = render('lifecycle');
const activation = html.slice(
html.indexOf('function activateLegendEntry'),
html.indexOf('function removeFlowOverlay'),
);
assert.match(activation, /select\(entry\.getAttribute\('data-legend-kind'\)\)/);
assert.match(activation, /open\(\{ opener: entry \}\)/);
assert.match(html, /event\.key === 'Enter' \|\| event\.key === ' '/);
assert.match(html, /event\.key === 'ArrowRight'/);
assert.match(html, /event\.key === 'ArrowLeft'/);
assert.match(html, /event\.key === 'Home'/);
assert.match(html, /event\.key === 'End'/);
assert.match(html, /lensOpener\.focus\(\)/);
assert.match(html, /entry\.setAttribute\('aria-pressed', selected \? 'true' : 'false'\)/);
});
test('bridge state is print-safe, reduced-motion-safe, and absent from canonical export', () => {
const html = render('architecture');
assert.match(html, /\[data-legend-bridge-runtime\] \{ display: none !important; \}/);
assert.match(html, /html:not\(\[data-embed="true"\]\) \.diagram-container \{\s*padding: 0\.75rem 0\.75rem 4\.25rem;/);
assert.match(html, /data-legend-hit[^}]*transition/s);
assert.match(html, /prefers-reduced-motion: reduce[\s\S]*\[data-legend-hit\]/);
assert.match(html, /clone\.removeAttribute\('data-legend-preview-active'\)/);
assert.match(html, /clone\.querySelectorAll\('\[data-legend-bridge-runtime\]'\)/);
assert.match(html, /el\.removeAttribute\('data-legend-kind'\)/);
assert.match(html, /el\.removeAttribute\('data-legend-label'\)/);
assert.match(html, /el\.removeAttribute\('data-legend-bridge'\)/);
assert.match(html, /\[data-legend-preview-match\], \[data-legend-preview-selected\], \[data-legend-preview-peer\]/);
assert.match(html, /\[data-legend-bridge\], \[data-legend-kind\], \[data-legend-bridge-runtime\]/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,103 @@
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-semantic-lens-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all typed renderers inherit one viewer-only Semantic Lens', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /id="semantic-lens" hidden role="dialog" aria-modal="false" aria-labelledby="semantic-lens-title"/, mode);
assert.match(html, /id="btn-semantic-lens"[^>]+aria-label="Open semantic lens"[^>]+aria-expanded="false"[^>]+aria-controls="semantic-lens"/, mode);
assert.match(html, /Archify\.semanticLens = \(function \(\)/, mode);
assert.match(html, /svg\.querySelectorAll\('\[data-node-id\]\[data-node-kind\]'\)/, mode);
assert.doesNotMatch(canonicalSvg(html), /semantic-lens-overlay|data-lens-active|data-lens-match/, mode);
}
});
test('Semantic Lens derives honest kind counts and compares at most two roles', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /function collectKinds\(\)/);
assert.match(html, /kind\.nodes\.push\(node\)/);
assert.match(html, /Choose up to two semantic kinds/);
assert.match(html, /if \(selectedKinds\.length >= 2\) return false/);
assert.match(html, /var crossKind = selectedKinds\.length === 2/);
assert.match(html, /fromKind === selectedKinds\[0\] && toKind === selectedKinds\[1\]/);
assert.match(html, /fromKind === selectedKinds\[1\] && toKind === selectedKinds\[0\]/);
assert.match(html, /direct relationship/);
assert.match(html, /data-lens-peer/);
assert.match(html, /data-lens-selected/);
});
test('Semantic Lens is shareable and yields cleanly to stronger reader intent', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /#lens=/);
assert.match(html, /params\.get\('lens'\)/);
assert.match(html, /window\.addEventListener\('hashchange', syncFromHash\)/);
assert.match(html, /event\.composedPath\(\)/);
assert.match(html, /eventPath\.indexOf\(panel\) >= 0/);
assert.match(html, /Archify\.semanticLens\.clear\(\{ updateUrl: false/);
assert.match(html, /Archify\.focus\.clear\(\{ updateUrl: false, preserveView: true \}\)/);
assert.match(html, /Archify\.routeProbe\.clear\(\{ updateUrl: false, restoreFocus: false \}\)/);
assert.match(html, /Archify\.guidedViews\.showAll\(\{ clearFocus: false, updateUrl: false \}\)/);
assert.match(html, /if \(action === 'lens'\) return Archify\.semanticLens\.open\(\)/);
assert.match(html, /e\.key === 'l' \|\| e\.key === 'L'/);
assert.match(html, /e\.key === 'Escape' && Archify\.semanticLens\.isOpen\(\)/);
assert.match(html, /e\.key === 'Escape' && Archify\.semanticLens\.active\(\)/);
});
test('Semantic Lens preserves Reading Depth, mobile containment, print, embed, and export boundaries', () => {
const html = render('dataflow', CASES.dataflow);
assert.match(html, /svg\[data-lens-active\] \[data-lens-match\] \[data-detail\]/);
assert.match(html, /svg\[data-lens-active\] \[data-lens-match\] \[data-detail-anchor\]/);
assert.match(html, /html\[data-embed="true"\] \.semantic-lens/);
assert.match(html, /data-wide-diagram="true"\] \.semantic-lens/);
assert.match(html, /@media print \{[\s\S]+svg\[data-lens-active\] \[data-node-id\][\s\S]+opacity: 1 !important/);
assert.match(html, /clone\.removeAttribute\('data-lens-active'\)/);
assert.match(html, /\[data-lens-match\], \[data-lens-selected\], \[data-lens-peer\]/);
assert.match(html, /clone\.querySelectorAll\('[^']*\[data-lens-match\][^']*\[data-lens-selected\][^']*\[data-lens-peer\][^']*'\)\.length === 0/);
assert.match(html, /class="semantic-lens no-print"/);
assert.doesNotMatch(canonicalSvg(html), /data-lens-active|data-lens-match|data-lens-selected|data-lens-peer/);
});
test('Semantic Lens docks away from selected nodes without breaking mobile containment', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /function overlapArea\(a, b\)/);
assert.match(html, /function dockPanel\(byId\)/);
assert.match(html, /selectedKinds\.indexOf\(byId\[id\]\.getAttribute\('data-node-kind'\)/);
assert.match(html, /var side = leftScore < rightScore \? 'left' : 'right'/);
assert.match(html, /panel\.setAttribute\('data-dock-side', side\)/);
assert.match(html, /\.semantic-lens\[data-dock-side="left"\]/);
assert.match(html, /@media \(max-width: 720px\)[\s\S]+\.semantic-lens\[data-dock-side\] \{ left: auto; right: 0\.5rem; \}/);
assert.match(html, /window\.addEventListener\('resize'/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,99 @@
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-semantic-passport-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function svg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all typed renderers emit details-on-demand metadata and native SVG titles', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
const diagram = svg(html);
assert.match(diagram, /data-node-kind="[^"]+"/, mode);
assert.match(diagram, /data-node-sublabel="[^"]+"/, mode);
assert.match(diagram, /data-node-context="[^"]+"/, mode);
assert.match(diagram, /<g id="node-[^"]+"[\s\S]*?<title>[^<]+ · [^<]+<\/title>/, mode);
}
});
test('renderer-owned structure supplies truthful Semantic Passport context', () => {
const architecture = render('architecture', CASES.architecture);
const workflow = render('workflow', CASES.workflow);
const sequence = render('sequence', CASES.sequence);
const dataflow = render('dataflow', CASES.dataflow);
const lifecycle = render('lifecycle', CASES.lifecycle);
assert.match(architecture, /data-node-id="api"[^>]+data-node-kind="backend"[^>]+data-node-context="AWS Region: us-west-2 sg-api :443\/:8000"/);
assert.match(workflow, /data-node-id="approval"[^>]+data-node-kind="security"[^>]+data-node-context="Policy &amp; Recovery Human or policy stop Plan \+ route"/);
assert.match(sequence, /data-node-id="redis"[^>]+data-node-kind="database"[^>]+data-node-context="Sequence participant"/);
assert.match(dataflow, /data-node-id="warehouse"[^>]+data-node-kind="database"[^>]+data-node-context="04 \/ Store"/);
assert.match(lifecycle, /data-node-id="executing"[^>]+data-node-kind="active"[^>]+data-node-context="Lifecycle phases"/);
});
test('Relationship Lens renders one Semantic Passport and copyable stable focus link', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /<span class="relationship-lens-eyebrow">Semantic passport<\/span>/);
assert.match(html, /id="focus-detail" hidden/);
assert.match(html, /id="focus-kind" data-passport="kind"/);
assert.match(html, /id="focus-context" data-passport="context" hidden/);
assert.match(html, /id="focus-tag" data-passport="tag" hidden/);
assert.match(html, /id="focus-id" data-passport="id"/);
assert.match(html, /id="btn-focus-clear"[^>]+aria-label="Close semantic passport"[^>]+title="Close">&#215;<\/button>/);
assert.match(html, /id="btn-focus-copy"[^>]+aria-label="Copy link to focused node"/);
assert.match(html, /id="btn-focus-relations"[^>]+aria-expanded="false"[^>]+aria-controls="relationship-lens-list"/);
assert.match(html, /function renderPassport\(id, node\)/);
assert.match(html, /var relationId = record && record\.id/);
assert.match(html, /\? '#relation=' \+ encodeURIComponent\(relationId\)/);
assert.match(html, /: '#focus=' \+ encodeURIComponent\(activeIds\[0\]\)/);
assert.match(html, /navigator\.clipboard\.writeText\(value\)/);
assert.match(html, /document\.execCommand\('copy'\)/);
assert.match(html, /copyLink: copyFocusLink/);
assert.match(html, /compactOnMobile = mobile && chip\.getAttribute\('data-relations-expanded'\) !== 'true'/);
assert.match(html, /nodeTop - chip\.offsetHeight - gap/);
assert.match(html, /focus-chip:not\(\[data-relations-expanded="true"\]\) \.relationship-lens-list \{ display: none; \}/);
assert.match(html, /clearBtn\.addEventListener\('click', function \(\) \{ clear\(\{ restoreFocus: true \}\); \}\)/);
assert.match(html, /chip\.hidden \|\| !target \|\| typeof target\.closest !== 'function' \|\| chip\.contains\(target\)/);
assert.match(html, /target\.closest\('\[data-node-id\], \[data-relationship-hit-key\], \.overview-map'\)/);
assert.match(html, /document\.addEventListener\('click',[\s\S]+?clear\(\);\s+\}, true\);/);
assert.match(html, /Archify\.focus\.clear\(\{ restoreFocus: true \}\)/);
});
test('Node Finder searches and presents the same passport facts', () => {
const html = render('dataflow', CASES.dataflow);
assert.match(html, /var authored = node\.getAttribute\('data-node-kind'\)/);
assert.match(html, /var sublabel = node\.getAttribute\('data-node-sublabel'\) \|\| ''/);
assert.match(html, /var context = node\.getAttribute\('data-node-context'\) \|\| ''/);
assert.match(html, /var tag = node\.getAttribute\('data-node-tag'\) \|\| ''/);
assert.match(html, /search: \(id \+ ' ' \+ label \+ ' ' \+ type \+ ' ' \+ sublabel \+ ' ' \+ context \+ ' ' \+ tag \+ ' ' \+ sourceSearch \+ ' ' \+ text\)\.toLowerCase\(\)/);
assert.match(html, /\[viewerKindLabel\(item\.type\), item\.id, item\.sublabel, item\.tag\]\.filter\(Boolean\)\.join\(' \\u00b7 '\)/);
assert.match(html, /meta\.title = \[viewerKindLabel\(item\.type\), item\.id, item\.context, item\.sublabel, item\.tag\]\.filter\(Boolean\)\.join\(' \\u00b7 '\)/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,575 @@
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, pathToFileURL } from 'node:url';
import { ChromeVisualBrowser, findChrome } 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-semantic-radar-'));
const chromePath = process.env.ARCHIFY_CHROME ? findChrome() : null;
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
async function evaluate(browser, sessionId, expression, awaitPromise = false) {
const response = await browser.cdp.send('Runtime.evaluate', {
expression,
awaitPromise,
returnByValue: true,
}, sessionId);
if (response.exceptionDetails) {
throw new Error(response.exceptionDetails.exception?.description
|| response.exceptionDetails.text
|| 'Runtime.evaluate failed');
}
return response.result?.value;
}
async function loadArtifact(browser, artifactPath, { width = 1440, height = 900 } = {}) {
const sessionId = await browser.sessionPromise;
await browser.cdp.send('Emulation.setDeviceMetricsOverride', {
width,
height,
deviceScaleFactor: 1,
mobile: false,
}, sessionId);
const loaded = browser.cdp.waitFor('Page.loadEventFired', sessionId);
const navigation = await browser.cdp.send('Page.navigate', {
url: pathToFileURL(artifactPath).href,
}, sessionId);
if (navigation.errorText) throw new Error(`Chrome navigation failed: ${navigation.errorText}`);
await loaded;
await evaluate(browser, sessionId, `(function () {
document.documentElement.setAttribute('data-motion', 'still');
var fontsReady = document.fonts && document.fonts.ready
? document.fonts.ready.catch(function () {})
: Promise.resolve();
return fontsReady.then(function () {
return new Promise(function (resolve) {
requestAnimationFrame(function () { requestAnimationFrame(resolve); });
});
});
})()`, true);
return sessionId;
}
async function radarRects(browser, sessionId, setup) {
return evaluate(browser, sessionId, `(function () {
${setup}
return new Promise(function (resolve) {
requestAnimationFrame(function () {
requestAnimationFrame(function () {
var radar = document.getElementById('overview-map').getBoundingClientRect();
var controls = document.querySelector('.diagram-nav').getBoundingClientRect();
var passport = document.getElementById('focus-chip');
var passportRect = passport && !passport.hidden ? passport.getBoundingClientRect() : null;
resolve({
radar: { left: radar.left, top: radar.top, right: radar.right, bottom: radar.bottom },
controls: { left: controls.left, top: controls.top, right: controls.right, bottom: controls.bottom },
passport: passportRect ? {
left: passportRect.left,
top: passportRect.top,
right: passportRect.right,
bottom: passportRect.bottom
} : null
});
});
});
});
})()`, true);
}
async function dragMouse(browser, sessionId, from, to) {
await browser.cdp.send('Input.dispatchMouseEvent', {
type: 'mousePressed',
x: from.x,
y: from.y,
button: 'left',
buttons: 1,
clickCount: 1,
}, sessionId);
await browser.cdp.send('Input.dispatchMouseEvent', {
type: 'mouseMoved',
x: to.x,
y: to.y,
button: 'left',
buttons: 1,
}, sessionId);
await browser.cdp.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
x: to.x,
y: to.y,
button: 'left',
buttons: 0,
clickCount: 1,
}, sessionId);
await evaluate(browser, sessionId, `new Promise(function (resolve) {
requestAnimationFrame(function () { requestAnimationFrame(resolve); });
})`, true);
}
function overlaps(a, b, gap = 0) {
return a.left < b.right + gap
&& a.right > b.left - gap
&& a.top < b.bottom + gap
&& a.bottom > b.top - gap;
}
test('all typed renderers inherit one viewer-only Semantic Radar', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /id="overview-map" hidden role="region" aria-labelledby="overview-map-title"/, mode);
assert.match(html, /id="overview-map-surface" tabindex="0" role="group"/, mode);
assert.match(html, /id="overview-map-expand"[^>]+aria-label="Open full semantic radar"/, mode);
assert.match(html, /id="overview-map-feedback" role="status" aria-live="polite" hidden/, mode);
assert.match(html, /id="btn-overview-map"[^>]+aria-label="Open semantic radar"[^>]+aria-expanded="false"[^>]+aria-controls="overview-map"/, mode);
assert.match(html, /Archify\.radar = \(function \(\)/, mode);
assert.match(html, /document\.createElementNS\(namespace, 'svg'\)/, mode);
assert.match(html, /mapSvg\.setAttribute\('aria-label', viewerText\('viewer\.radar\.nodes'\)\)/, mode);
assert.match(html, /diagram\.querySelectorAll\('\[data-node-id\]'\)/, mode);
assert.equal((html.match(/<svg\b/g) || []).length, 1, `${mode} keeps one static canonical SVG`);
assert.doesNotMatch(canonicalSvg(html), /overview-map|Semantic radar|data-radar-node-id/, mode);
}
});
test('Semantic Radar derives semantic node bounds and focuses stable IDs', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /box = node\.getBBox\(\)/);
assert.match(html, /rect\.setAttribute\('data-radar-node-id', id\)/);
assert.match(html, /rect\.setAttribute\('data-kind', node\.getAttribute\('data-node-kind'\) \|\| 'neutral'\)/);
assert.match(html, /rect\.setAttribute\('aria-label', viewerText\('viewer\.radar\.focus'/);
assert.match(html, /Archify\.focus\.set\(id, \{ toggle: false \}\)/);
assert.match(html, /Archify\.view\.reveal\(\[id\], \{ includeNeighbors: true, reason: 'radar' \}\)/);
assert.match(html, /function bringNodeIntoWindow\(node\)/);
assert.match(html, /window\.scrollY \+ rect\.top \+ rect\.height \/ 2 - window\.innerHeight \/ 2/);
assert.match(html, /data-radar-active/);
});
test('Semantic Radar tracks desktop camera and mobile contained scroll', () => {
const html = render('sequence', CASES.sequence);
assert.match(html, /function logicalViewport\(\)/);
assert.match(html, /x = viewBox\.x \+ container\.scrollLeft \/ metrics\.scale/);
assert.match(html, /x = viewBox\.x \+ \(\(-state\.x \/ state\.scale\) - metrics\.offsetX\) \/ metrics\.scale/);
assert.match(html, /viewport\.setAttribute\('width', String\(visible\.width\)\)/);
assert.match(html, /viewerText\('viewer\.radar\.viewport\.width'/);
assert.match(html, /function centerAt\(logicalX, logicalY, options\)/);
assert.match(html, /minimumScale: 1\.5, instant: true/);
assert.match(html, /container\.scrollTo\(\{ left: mobileTarget, behavior: options\.instant \? 'auto' : 'smooth' \}\)/);
assert.match(html, /data-wide-diagram="true"\] \.overview-map/);
assert.match(html, /function updateDocking\(\)/);
assert.match(html, /chip\.style\.top = Math\.round\(top\) \+ 'px';[\s\S]+Archify\.radar\.sync\(\)/);
assert.match(html, /var navigation = container\.querySelector\('\.diagram-nav'\)/);
assert.match(html, /if \(controlRect\) bottom = Math\.min\(bottom, controlRect\.top - placementGap\)/);
assert.match(html, /hardBlockers: \[lensRect, controlRect, legendRect\]\.filter\(Boolean\)/);
assert.match(html, /function cornerCandidates\(context\)/);
assert.match(html, /function nearbyCandidates\(context, reference\)/);
assert.match(html, /nearbyCandidates\(context, reference\)\.concat\(cornerCandidates\(context\)\)/);
assert.match(html, /var placementOptions = \{ softWeight: manualPosition \? 0 : 100 \}/);
assert.match(html, /manualPosition && positionIsValid\(manualPosition, context\)/);
assert.match(html, /panelHead\.addEventListener\('pointerdown', beginPanelDrag\)/);
assert.match(html, /surface\.addEventListener\('pointerdown',[\s\S]+viewportDrag = \{ pointerId: event\.pointerId \}/);
assert.match(html, /target\.closest\('\[data-node-id\], \[data-relationship-hit-key\], \.overview-map'\)/);
assert.match(html, /--archify-radar-top/);
assert.match(html, /\.overview-map\[data-docked="true"\]/);
});
test('Semantic Radar keeps redundant accessible navigation and clean exports', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /Semantic radar \(M\)/);
assert.match(html, /e\.key === 'm' \|\| e\.key === 'M'/);
assert.match(html, /e\.key === 'Escape' && Archify\.radar\.isOpen\(\)/);
assert.match(html, /event\.key === 'ArrowLeft'[\s\S]+event\.key === 'ArrowRight'[\s\S]+event\.key === 'ArrowUp'[\s\S]+event\.key === 'ArrowDown'/);
assert.match(html, /node && \(event\.key === 'Enter' \|\| event\.key === ' '\)/);
assert.match(html, /\.overview-map-viewport \{[\s\S]*?pointer-events: none;/);
assert.match(html, /html\[data-embed="true"\] \.overview-map/);
assert.match(html, /class="overview-map no-print"/);
assert.match(html, /The radar is built at runtime so the checked artifact still contains[\s\S]+one canonical SVG block/);
assert.doesNotMatch(canonicalSvg(html), /overview-map-node|overview-map-viewport/);
});
test('Semantic Radar stays above the measured MAP control strip', {
skip: chromePath ? false : 'Set ARCHIFY_CHROME to run the real browser regression.',
}, async () => {
const artifact = path.join(tmp, 'radar-control-clearance.html');
execFileSync(process.execPath, [
path.join(skillRoot, 'renderers/architecture/render-architecture.mjs'),
path.join(skillRoot, 'examples', CASES.architecture),
artifact,
]);
const browser = new ChromeVisualBrowser(chromePath);
try {
const sessionId = await loadArtifact(browser, artifact, { width: 1440, height: 900 });
const rects = await radarRects(browser, sessionId, `
var container = document.querySelector('.diagram-container');
window.scrollTo(0, Math.max(0, container.offsetTop + container.offsetHeight - window.innerHeight + 8));
Archify.radar.open();
`);
const controlGap = rects.controls.top - rects.radar.bottom;
assert.ok(controlGap >= 15, JSON.stringify({ ...rects, controlGap }, null, 2));
assert.ok(
rects.radar.left < rects.controls.left,
`automatic placement should prefer the lower-left corner: ${JSON.stringify(rects, null, 2)}`,
);
} finally {
await browser.close();
}
});
test('Semantic Radar avoids an expanded mobile Passport without hiding a collision', {
skip: chromePath ? false : 'Set ARCHIFY_CHROME to run the real browser regression.',
}, async () => {
const artifact = path.join(tmp, 'radar-mobile-passport.html');
execFileSync(process.execPath, [
path.join(skillRoot, 'renderers/architecture/render-architecture.mjs'),
path.join(skillRoot, 'examples', CASES.architecture),
artifact,
]);
const browser = new ChromeVisualBrowser(chromePath);
try {
const sessionId = await loadArtifact(browser, artifact, { width: 390, height: 600 });
const state = await evaluate(browser, sessionId, `(function () {
var container = document.querySelector('.diagram-container');
window.scrollTo(0, Math.max(0, container.offsetTop));
Archify.focus.set('lb', { toggle: false });
document.getElementById('btn-focus-relations').click();
Archify.radar.open();
return new Promise(function (resolve) {
setTimeout(function () {
var radar = document.getElementById('overview-map');
var radarRect = radar.getBoundingClientRect();
var passportRect = document.getElementById('focus-chip').getBoundingClientRect();
var containerRect = document.querySelector('.diagram-container').getBoundingClientRect();
var controlsRect = document.querySelector('.diagram-nav').getBoundingClientRect();
var legendRect = document.querySelector('[data-legend]').getBoundingClientRect();
resolve({
radar: { left: radarRect.left, top: radarRect.top, right: radarRect.right, bottom: radarRect.bottom },
passport: { left: passportRect.left, top: passportRect.top, right: passportRect.right, bottom: passportRect.bottom },
container: { left: containerRect.left, top: containerRect.top, right: containerRect.right, bottom: containerRect.bottom },
controls: { left: controlsRect.left, top: controlsRect.top, right: controlsRect.right, bottom: controlsRect.bottom },
legend: { left: legendRect.left, top: legendRect.top, right: legendRect.right, bottom: legendRect.bottom },
viewport: { width: window.innerWidth, height: window.innerHeight },
invalid: radar.getAttribute('data-placement-invalid'),
compact: radar.getAttribute('data-compact'),
unavailable: radar.getAttribute('data-placement-unavailable')
});
}, 180);
});
})()`, true);
assert.equal(overlaps(state.radar, state.passport, 10), false, JSON.stringify(state, null, 2));
assert.notEqual(state.invalid, 'true', JSON.stringify(state, null, 2));
assert.equal(state.compact, 'true', JSON.stringify(state, null, 2));
const expanded = await evaluate(browser, sessionId, `(function () {
document.getElementById('overview-map-expand').click();
return new Promise(function (resolve) {
setTimeout(function () {
var radar = document.getElementById('overview-map');
var rect = radar.getBoundingClientRect();
var passport = document.getElementById('focus-chip');
resolve({
compact: radar.getAttribute('data-compact'),
height: rect.height,
surfaceVisible: getComputedStyle(document.getElementById('overview-map-surface')).display !== 'none',
passportYielded: passport.getAttribute('data-radar-yielded'),
passportVisible: getComputedStyle(passport).display !== 'none'
});
}, 120);
});
})()`, true);
assert.equal(expanded.compact, null, JSON.stringify(expanded, null, 2));
assert.equal(expanded.surfaceVisible, true, JSON.stringify(expanded, null, 2));
assert.equal(expanded.passportYielded, 'true', JSON.stringify(expanded, null, 2));
assert.equal(expanded.passportVisible, false, JSON.stringify(expanded, null, 2));
assert.ok(expanded.height > state.radar.bottom - state.radar.top, JSON.stringify({ state, expanded }, null, 2));
const closed = await evaluate(browser, sessionId, `(function () {
document.getElementById('overview-map-close').click();
var passport = document.getElementById('focus-chip');
return {
radarHidden: document.getElementById('overview-map').hidden,
passportYielded: passport.getAttribute('data-radar-yielded'),
passportVisible: getComputedStyle(passport).display !== 'none'
};
})()`);
assert.equal(closed.radarHidden, true, JSON.stringify(closed, null, 2));
assert.equal(closed.passportYielded, null, JSON.stringify(closed, null, 2));
assert.equal(closed.passportVisible, true, JSON.stringify(closed, null, 2));
} finally {
await browser.close();
}
});
test('Semantic Radar reports a consistent unavailable state and recovers when space returns', {
skip: chromePath ? false : 'Set ARCHIFY_CHROME to run the real browser regression.',
}, async () => {
const artifact = path.join(tmp, 'radar-unavailable.html');
execFileSync(process.execPath, [
path.join(skillRoot, 'renderers/architecture/render-architecture.mjs'),
path.join(skillRoot, 'examples', CASES.architecture),
artifact,
]);
const browser = new ChromeVisualBrowser(chromePath);
try {
const sessionId = await loadArtifact(browser, artifact, { width: 390, height: 300 });
const unavailable = await evaluate(browser, sessionId, `(function () {
var container = document.querySelector('.diagram-container');
window.scrollTo(0, Math.max(0, container.offsetTop));
Archify.focus.set('lb', { toggle: false });
document.getElementById('btn-focus-relations').click();
Archify.radar.open();
return new Promise(function (resolve) {
setTimeout(function () {
var panel = document.getElementById('overview-map');
var trigger = document.getElementById('btn-overview-map');
var feedback = document.getElementById('overview-map-feedback');
resolve({
panelHidden: panel.hidden,
expanded: trigger.getAttribute('aria-expanded'),
limited: trigger.getAttribute('data-radar-space-limited'),
feedbackHidden: feedback.hidden,
feedback: feedback.textContent.trim()
});
}, 260);
});
})()`, true);
assert.equal(unavailable.panelHidden, true, JSON.stringify(unavailable, null, 2));
assert.equal(unavailable.expanded, 'false', JSON.stringify(unavailable, null, 2));
assert.equal(unavailable.limited, 'true', JSON.stringify(unavailable, null, 2));
assert.equal(unavailable.feedbackHidden, false, JSON.stringify(unavailable, null, 2));
assert.match(unavailable.feedback, /space/i);
await browser.cdp.send('Emulation.setDeviceMetricsOverride', {
width: 390,
height: 600,
deviceScaleFactor: 1,
mobile: false,
}, sessionId);
const recovered = await evaluate(browser, sessionId, `new Promise(function (resolve) {
setTimeout(function () {
var panel = document.getElementById('overview-map');
var trigger = document.getElementById('btn-overview-map');
var feedback = document.getElementById('overview-map-feedback');
resolve({
panelHidden: panel.hidden,
expanded: trigger.getAttribute('aria-expanded'),
feedbackHidden: feedback.hidden
});
}, 260);
})`, true);
assert.equal(recovered.panelHidden, false, JSON.stringify(recovered, null, 2));
assert.equal(recovered.expanded, 'true', JSON.stringify(recovered, null, 2));
assert.equal(recovered.feedbackHidden, true, JSON.stringify(recovered, null, 2));
} finally {
await browser.close();
}
});
test('Semantic Radar automatically avoids a tall Semantic Passport', {
skip: chromePath ? false : 'Set ARCHIFY_CHROME to run the real browser regression.',
}, async () => {
const input = path.join(tmp, 'tall-passport.architecture.json');
const artifact = path.join(tmp, 'tall-passport.html');
const peers = Array.from({ length: 12 }, (_, index) => ({
id: `peer-${index + 1}`,
type: index % 2 ? 'backend' : 'database',
label: `Peer ${index + 1}`,
sublabel: 'Connected system',
pos: [80, 40 + index * 90],
size: [130, 60],
}));
fs.writeFileSync(input, JSON.stringify({
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Tall Passport Radar Regression', output: artifact },
components: [
...peers,
{ id: 'hub', type: 'security', label: 'Relationship Hub', sublabel: 'Many authored links', pos: [900, 500], size: [150, 70] },
],
boundaries: [],
connections: peers.map((peer, index) => ({
id: `hub-to-${peer.id}`,
from: 'hub',
to: peer.id,
fromSide: 'left',
toSide: 'right',
via: [[840, 535], [840, peer.pos[1] + 30]],
})),
cards: [],
}, null, 2));
execFileSync(process.execPath, [
path.join(skillRoot, 'renderers/architecture/render-architecture.mjs'),
input,
artifact,
]);
const browser = new ChromeVisualBrowser(chromePath);
try {
const sessionId = await loadArtifact(browser, artifact, { width: 1200, height: 700 });
const rects = await radarRects(browser, sessionId, `
var container = document.querySelector('.diagram-container');
window.scrollTo(0, Math.max(0, container.offsetTop));
Archify.focus.set('hub', { toggle: false });
Archify.radar.open();
`);
assert.ok(rects.passport, JSON.stringify(rects, null, 2));
assert.equal(overlaps(rects.radar, rects.passport, 10), false, JSON.stringify(rects, null, 2));
const dragGeometry = await evaluate(browser, sessionId, `(function () {
var radar = document.getElementById('overview-map').getBoundingClientRect();
var head = document.querySelector('.overview-map-head').getBoundingClientRect();
var passport = document.getElementById('focus-chip').getBoundingClientRect();
var active = document.querySelector('[data-focus-selected]');
var nearestLeft = passport.right + 16;
active.getBoundingClientRect = function () {
return {
left: nearestLeft,
top: radar.top,
right: nearestLeft + radar.width,
bottom: radar.top + radar.height,
width: radar.width,
height: radar.height
};
};
return {
radar: { left: radar.left, top: radar.top },
head: { left: head.left, top: head.top, height: head.height },
requested: { left: passport.right + 8, top: radar.top },
nearest: { left: nearestLeft, top: radar.top }
};
})()`);
await dragMouse(browser, sessionId, {
x: dragGeometry.head.left + 48,
y: dragGeometry.head.top + dragGeometry.head.height / 2,
}, {
x: dragGeometry.head.left + 48 + dragGeometry.requested.left - dragGeometry.radar.left,
y: dragGeometry.head.top + dragGeometry.head.height / 2 + dragGeometry.requested.top - dragGeometry.radar.top,
});
const snappedRects = await radarRects(browser, sessionId, '');
assert.equal(overlaps(snappedRects.radar, snappedRects.passport, 10), false, JSON.stringify(snappedRects, null, 2));
assert.ok(Math.abs(snappedRects.radar.left - dragGeometry.nearest.left) <= 2, JSON.stringify({ dragGeometry, snappedRects }, null, 2));
assert.ok(Math.abs(snappedRects.radar.top - dragGeometry.nearest.top) <= 2, JSON.stringify({ dragGeometry, snappedRects }, null, 2));
} finally {
await browser.close();
}
});
test('Semantic Radar titlebar drag persists while surface drag still pans the diagram', {
skip: chromePath ? false : 'Set ARCHIFY_CHROME to run the real browser regression.',
}, async () => {
const artifact = path.join(tmp, 'radar-dragging.html');
execFileSync(process.execPath, [
path.join(skillRoot, 'renderers/architecture/render-architecture.mjs'),
path.join(skillRoot, 'examples', CASES.architecture),
artifact,
]);
const browser = new ChromeVisualBrowser(chromePath);
try {
const sessionId = await loadArtifact(browser, artifact, { width: 1440, height: 900 });
const geometry = await evaluate(browser, sessionId, `(function () {
var container = document.querySelector('.diagram-container');
window.scrollTo(0, Math.max(0, container.offsetTop + container.offsetHeight - window.innerHeight + 8));
Archify.radar.open();
var radar = document.getElementById('overview-map').getBoundingClientRect();
var head = document.querySelector('.overview-map-head').getBoundingClientRect();
var containerRect = container.getBoundingClientRect();
return {
radar: { left: radar.left, top: radar.top, width: radar.width, height: radar.height },
head: { left: head.left, top: head.top, width: head.width, height: head.height },
state: Archify.view.state(),
target: {
left: Math.max(24, containerRect.left + 360),
top: Math.max(24, containerRect.top + 20)
}
};
})()`);
const titleStart = {
x: geometry.head.left + 48,
y: geometry.head.top + geometry.head.height / 2,
};
const titleTarget = {
x: titleStart.x + geometry.target.left - geometry.radar.left,
y: titleStart.y + geometry.target.top - geometry.radar.top,
};
await dragMouse(browser, sessionId, titleStart, titleTarget);
const manuallyPlaced = await evaluate(browser, sessionId, `(function () {
Archify.radar.sync();
var radar = document.getElementById('overview-map').getBoundingClientRect();
return { left: radar.left, top: radar.top, state: Archify.view.state() };
})()`);
assert.ok(Math.abs(manuallyPlaced.left - geometry.target.left) <= 2, JSON.stringify({ geometry, manuallyPlaced }, null, 2));
assert.ok(Math.abs(manuallyPlaced.top - geometry.target.top) <= 2, JSON.stringify({ geometry, manuallyPlaced }, null, 2));
assert.deepEqual(manuallyPlaced.state, geometry.state);
const surfaceState = await evaluate(browser, sessionId, `(function () {
var radar = document.getElementById('overview-map').getBoundingClientRect();
var surface = document.getElementById('overview-map-surface').getBoundingClientRect();
return {
radar: { left: radar.left, top: radar.top },
state: Archify.view.state(),
start: { x: surface.left + 8, y: surface.top + 8 },
end: { x: surface.right - 8, y: surface.bottom - 8 }
};
})()`);
await dragMouse(browser, sessionId, surfaceState.start, surfaceState.end);
const afterSurfaceDrag = await evaluate(browser, sessionId, `(function () {
var radar = document.getElementById('overview-map').getBoundingClientRect();
return {
radar: { left: radar.left, top: radar.top },
state: Archify.view.state()
};
})()`);
assert.deepEqual(afterSurfaceDrag.radar, surfaceState.radar);
assert.notDeepEqual(afterSurfaceDrag.state, surfaceState.state);
await browser.cdp.send('Emulation.setDeviceMetricsOverride', {
width: 640,
height: 700,
deviceScaleFactor: 1,
mobile: false,
}, sessionId);
const afterResize = await evaluate(browser, sessionId, `new Promise(function (resolve) {
setTimeout(function () {
var radar = document.getElementById('overview-map').getBoundingClientRect();
var controls = document.querySelector('.diagram-nav').getBoundingClientRect();
var container = document.querySelector('.diagram-container').getBoundingClientRect();
resolve({
radar: { left: radar.left, top: radar.top, right: radar.right, bottom: radar.bottom },
controls: { left: controls.left, top: controls.top, right: controls.right, bottom: controls.bottom },
container: { left: container.left, top: container.top, right: container.right, bottom: container.bottom },
viewport: { width: window.innerWidth, height: window.innerHeight }
});
}, 120);
})`, true);
assert.ok(afterResize.radar.left >= Math.max(0, afterResize.container.left), JSON.stringify(afterResize, null, 2));
assert.ok(afterResize.radar.right <= Math.min(afterResize.viewport.width, afterResize.container.right), JSON.stringify(afterResize, null, 2));
assert.equal(overlaps(afterResize.radar, afterResize.controls, 10), false, JSON.stringify(afterResize, null, 2));
} finally {
await browser.close();
}
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,86 @@
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-semantic-zoom-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function svg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all typed renderers emit explicit context and fine reading-depth semantics', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /data-detail="context"/, mode);
assert.match(html, /data-detail="fine"/, mode);
assert.match(html, /data-detail-anchor/, mode);
assert.doesNotMatch(html, /<text[^>]*data-detail="(?:context|fine)"[^>]*class="t-primary"/, mode);
assert.match(html, /class="diagram-container" data-detail-level="read"/, mode);
}
});
test('semantic zoom exposes MAP, READ, and FULL at deterministic thresholds', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /function detailLevel\(\)/);
assert.match(html, /if \(state\.mode === 'semantic'\) return 'full'/);
assert.match(html, /if \(state\.scale >= 1\.75\) return 'full'/);
assert.match(html, /if \(state\.scale >= 1\) return 'read'/);
assert.match(html, /return 'map'/);
assert.match(html, /container\.setAttribute\('data-detail-level', detail\)/);
assert.match(html, /var levelLabel = viewerText\('viewer\.nav\.level\.' \+ detail\)/);
assert.match(html, /var resolvedLevel = semantic \? viewerText\('viewer\.nav\.level\.auto'\) : levelLabel/);
assert.match(html, /Zoom in to reveal relationship labels and node context/);
assert.match(html, /Zoom in again to reveal tags and annotations/);
assert.match(html, /Full diagram detail/);
});
test('reading depth stays quiet at overview and yields to semantic intent', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /\.diagram-container\[data-detail-level="map"\] svg \[data-detail="context"\]/);
assert.match(html, /\.diagram-container\[data-detail-level="map"\] svg \[data-detail="fine"\]/);
assert.match(html, /\.diagram-container\[data-detail-level="read"\] svg \[data-detail="fine"\]/);
assert.match(html, /\.diagram-container\[data-detail-level="map"\] svg \[data-detail-anchor\]/);
assert.match(html, /svg\[data-focus-active\] \[data-focus-match\] \[data-detail\]/);
assert.match(html, /svg\[data-intent-trace-active\] \[data-intent-trace-match\] \[data-detail\]/);
assert.match(html, /svg\[data-route-active\] \[data-route-match\] \[data-detail\]/);
assert.match(html, /svg\[data-story-active\] \[data-story-step\] \[data-detail\]/);
assert.match(html, /svg\[data-relationship-preview-active\] \[data-relationship-preview\] \[data-detail\]/);
});
test('semantic zoom is motion-safe and full-fidelity in print and export', () => {
const html = render('dataflow', CASES.dataflow);
assert.match(html, /\[data-detail\] \{ transition: opacity 160ms ease/);
assert.match(html, /@media print \{[\s\S]+\.diagram-container svg \[data-detail\] \{[\s\S]+opacity: 1 !important/);
assert.match(html, /@media \(prefers-reduced-motion: reduce\)[\s\S]+svg \[data-detail\]/);
assert.match(html, /clone\.querySelectorAll\('\[data-detail\], \[data-detail-anchor\]'\)/);
assert.match(html, /el\.removeAttribute\('data-detail'\)/);
assert.match(html, /el\.removeAttribute\('data-detail-anchor'\)/);
assert.match(html, /clone\.querySelectorAll\('[^']*\[data-detail\][^']*\[data-detail-anchor\][^']*'\)\.length === 0/);
assert.doesNotMatch(svg(html), /data-detail-level=/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,137 @@
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';
import { textUnits } from '../renderers/shared/utils.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
function renderOutcome(doc) {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-column-fit-'));
const input = path.join(tmp, 'input.json');
const output = path.join(tmp, 'output.html');
fs.writeFileSync(input, JSON.stringify(doc));
try {
execFileSync('node', [
path.join(skillRoot, 'renderers/sequence/render-sequence.mjs'),
input,
output,
], { stdio: ['ignore', 'ignore', 'pipe'] });
return { code: 0, stderr: '', html: fs.readFileSync(output, 'utf8') };
} catch (err) {
return { code: err.status ?? 1, stderr: String(err.stderr || ''), html: '' };
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
}
function render(doc) {
const outcome = renderOutcome(doc);
assert.equal(outcome.code, 0, outcome.stderr);
return outcome.html;
}
function participantBoxes(html) {
return [...html.matchAll(/<rect x="([\d.]+)" y="72" width="([\d.]+)" height="54"/g)]
.map(([, x, width]) => ({ x: Number(x), width: Number(width) }))
.filter((box, index, all) => all.findIndex((other) => other.x === box.x) === index)
.sort((left, right) => left.x - right.x);
}
function wideSequence(columnFit) {
const meta = { title: 'Column fit', viewBox: [1320, 620] };
if (columnFit) meta.column_fit = columnFit;
return {
schema_version: 1,
diagram_type: 'sequence',
meta,
participants: [
{ id: 'browser', type: 'frontend', label: 'Browser' },
{ id: 'gateway', type: 'backend', label: 'Gateway' },
{ id: 'idp', type: 'security', label: 'IdP' },
{ id: 'api', type: 'backend', label: 'API' },
{ id: 'store', type: 'database', label: 'Store' }
],
messages: [
{ from: 'browser', to: 'gateway', y: 200, label: 'request' },
{ from: 'gateway', to: 'idp', y: 260, label: 'authorize' },
{ from: 'idp', to: 'api', y: 320, label: 'token' },
{ from: 'api', to: 'store', y: 380, label: 'read' }
]
};
}
test('fixed column fit keeps the historical 108px gap regardless of viewBox width', () => {
const boxes = participantBoxes(render(wideSequence()));
assert.equal(boxes.length, 5);
assert.equal(boxes[0].width, 86);
assert.equal(boxes[1].x - boxes[0].x, 108);
assert.equal(boxes.at(-1).x + boxes.at(-1).width < 600, true,
'fixed lanes stay packed on the left, leaving the wide canvas unused');
});
test('spread column fit uses the viewBox width and stays inside it', () => {
const boxes = participantBoxes(render(wideSequence('spread')));
assert.equal(boxes.length, 5);
assert.ok(boxes[0].width > 86, 'participant boxes widen with the available room');
assert.ok(boxes[1].x - boxes[0].x > 108, 'columns spread past the fixed gap');
assert.equal(boxes[0].x, 62, 'first lane keeps the side margin');
assert.ok(boxes.at(-1).x + boxes.at(-1).width <= 1320 - 40,
'last lane stays inside the viewBox with the reserved margin');
});
test('spread column fit is opt-in, so an unset value renders like fixed', () => {
assert.equal(render(wideSequence()), render(wideSequence('fixed')));
});
const wideLabel = 'Payment Gateway Service';
function labelledSequence(columnFit) {
const doc = wideSequence(columnFit);
doc.participants[1].label = wideLabel;
return doc;
}
test('a label the fixed box rejects fits the spread box on the same viewBox', () => {
const estimatedLabelW = textUnits(wideLabel) * 6.8;
assert.ok(estimatedLabelW > 86 + 6, 'the fixture label must actually exceed the fixed box');
const fixed = renderOutcome(labelledSequence());
assert.notEqual(fixed.code, 0, 'the fixed box still rejects a label it cannot hold');
assert.ok(fixed.stderr.includes(`Label "${wideLabel}"`), `expected the label in stderr:\n${fixed.stderr}`);
assert.ok(fixed.stderr.includes('86px participant box'), `expected the fixed box width in stderr:\n${fixed.stderr}`);
const spread = renderOutcome(labelledSequence('spread'));
assert.equal(spread.code, 0, spread.stderr);
const box = participantBoxes(spread.html)[1];
assert.ok(estimatedLabelW <= box.width + 6, `label ~${estimatedLabelW}px must fit the ${box.width}px spread box`);
assert.ok(spread.html.includes(`>${wideLabel}</text>`), 'the label renders unshortened');
});
test('the sublabel diagnostic reports the width in force, not the historical constant', () => {
const unrescuable = 'Payment authorization gateway detail text that stays far too long to shrink';
const doc = wideSequence('spread');
doc.participants[0].sublabel = unrescuable;
const { code, stderr } = renderOutcome(doc);
assert.notEqual(code, 0, 'a sublabel past the legible minimum is still rejected');
assert.match(stderr, /participant boxes are 190px for this viewBox width and 5 participants/);
assert.doesNotMatch(stderr, /boxes are a fixed/, 'spread must not quote the fixed layout');
});
test('the fast authoring path explains when to opt into spread', () => {
const schema = JSON.parse(fs.readFileSync(path.join(skillRoot, 'schemas/sequence.schema.json'), 'utf8'));
const description = schema.properties.meta.properties.column_fit.description;
const skill = fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8');
const rendererReadme = fs.readFileSync(path.join(skillRoot, 'renderers/sequence/README.md'), 'utf8');
assert.match(description, /wide viewBox/);
assert.match(description, /meaningful participant labels/);
assert.match(skill, /do not shorten semantic labels before trying `spread`/);
assert.match(rendererReadme, /Use `"spread"` when a wide/);
assert.match(rendererReadme, /try `meta\.column_fit: "spread"` before shortening/);
});
@@ -0,0 +1,97 @@
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';
import { animateAttr } from '../renderers/shared/cli.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-settled-flow-'));
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, example) {
const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', example), 'utf8'));
doc.meta = { ...doc.meta, animation: 'trace' };
const input = path.join(tmp, `${mode}.json`);
const output = path.join(tmp, `${mode}.html`);
fs.writeFileSync(input, JSON.stringify(doc));
execFileSync('node', [path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`), input, output], {
stdio: ['ignore', 'ignore', 'pipe'],
});
return fs.readFileSync(output, 'utf8');
}
test('all five renderers inherit one finite running-to-settled ambient contract', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /data-ambient-motion/, mode);
assert.match(html, /function startAmbient\(\)/, mode);
assert.match(html, /function settleAmbient\(reason\)/, mode);
assert.match(html, /animation: archify-edge-flow [^;]+ 1;/, mode);
assert.match(html, /animation: archify-node-pulse [^;]+ 1;/, mode);
assert.doesNotMatch(html, /animation: archify-edge-flow [^;]+ infinite/, mode);
assert.doesNotMatch(html, /animation: archify-node-pulse [^;]+ infinite/, mode);
}
});
test('settled flow restores authored security and async dash semantics', () => {
assert.match(template, /\.a-security\s*\{[^}]*stroke-dasharray:\s*5,5/);
assert.match(template, /\.a-dashed\s*\{[^}]*stroke-dasharray:\s*4,4/);
assert.match(template, /@keyframes archify-edge-flow\s*\{[\s\S]*?stroke-dasharray:\s*10 8/);
assert.match(template, /100%\s*\{\s*stroke-dashoffset:\s*0;\s*opacity:\s*1;\s*\}/);
const runningRule = template.match(/html\[data-ambient-motion="running"\][^{]+\[data-animate="edge"\][^{]*\{([^}]*)\}/)?.[1] || '';
assert.ok(runningRule, 'running edge rule missing');
assert.doesNotMatch(runningRule, /stroke-dasharray|stroke-dashoffset/);
});
test('ambient ownership is generation-bounded and cannot replay after settle', () => {
assert.match(template, /var ambientStarted = false/);
assert.match(template, /var ambientPending = new Set\(\)/);
assert.match(template, /if \(ambientStarted \|\| !capable\) return false/);
assert.match(template, /ambientStarted = true;[\s\S]*?html\.setAttribute\('data-ambient-motion', 'running'\)/);
assert.match(template, /ambientPending\.delete\(event\.target\)/);
assert.match(template, /if \(!ambientPending\.size\) settleAmbient\('complete'\)/);
assert.match(template, /svg\.addEventListener\('animationend', onAmbientBoundary, true\)/);
assert.match(template, /svg\.addEventListener\('animationcancel', onAmbientBoundary, true\)/);
assert.match(template, /if \(paused \|\| owner \|\| html\.hasAttribute\('data-embed'\)/);
assert.doesNotMatch(template, /setInterval\([^)]*ambient|addEventListener\('scroll'[^)]*ambient/);
});
test('animation delay is capped without changing normal authored order', () => {
assert.equal(animateAttr({ animation: 'trace' }, 'edge', 0), ' data-animate="edge" style="--step:0"');
assert.equal(animateAttr({ animation: 'trace' }, 'node', 8), ' data-animate="node" style="--step:8"');
assert.equal(animateAttr({ animation: 'trace' }, 'edge', 99), ' data-animate="edge" style="--step:12"');
assert.equal(animateAttr({}, 'edge', 99), '');
});
test('only the WebM canvas scene opts into a repeatable finite motion timeline', () => {
assert.match(template, /var motionScene = createMotionScene\(svg\)/);
assert.match(template, /drawMotionFrame\(ctx, backgroundImage, motionScene, elapsed\)/);
assert.match(template, /var data = serializeSvg\(scale\);/);
assert.match(template, /getPointAtLength/);
assert.doesNotMatch(template, /serializeSvg\(1, \{ autoTheme: true, motion: true \}\)/);
});
test('Still, reduced motion, embed, share, hidden state, and stronger intent settle ambient flow', () => {
assert.match(template, /reducedMotion\(\)/);
assert.match(template, /html\.hasAttribute\('data-embed'\)/);
assert.match(template, /html\.hasAttribute\('data-share-playback'\)/);
assert.match(template, /html\.hasAttribute\('data-document-hidden'\)/);
assert.match(template, /paused \|\| owner/);
assert.match(template, /settleAmbient\('suppressed'\)/);
assert.match(template, /html\[data-motion="still"\] svg\[data-animation="trace"\] \[data-animate\]/);
assert.match(template, /@media \(prefers-reduced-motion: reduce\)/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,132 @@
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 here = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(here, '..');
const repoRoot = path.resolve(skillRoot, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-share-card-'));
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) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', CASES[mode]),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function svgBlock(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all five renderers expose one explicit 1200x630 Share Card export', () => {
for (const mode of Object.keys(CASES)) {
const html = render(mode);
assert.match(html, /data-format="share-card"/, mode);
assert.match(html, /Share Card[\s\S]*?1200(?:&times;|×)630 PNG/, mode);
assert.match(html, /var SHARE_CARD_WIDTH = 1200;/, mode);
assert.match(html, /var SHARE_CARD_HEIGHT = 630;/, mode);
assert.match(html, /function rasterizeShareCard\(options\)/, mode);
assert.match(html, /format === 'share-card'/, mode);
}
});
test('Share Card uses contain-only canonical geometry with fixed safe areas', () => {
const html = render('architecture');
assert.match(html, /var availableWidth = SHARE_CARD_WIDTH - SHARE_CARD_PADDING \* 2;/);
assert.match(html, /var availableHeight = SHARE_CARD_HEIGHT - SHARE_CARD_HEADER - SHARE_CARD_PADDING;/);
assert.match(html, /var fit = Math\.min\(availableWidth \/ data\.width, availableHeight \/ data\.height\);/);
assert.match(html, /ctx\.drawImage\(img, drawX, drawY, drawWidth, drawHeight\);/);
assert.match(html, /function canvas2dOrThrow\(canvas, label\)/);
assert.match(html, /throw exportError\('viewer\.export\.error\.contextUnavailable'/);
assert.match(html, /throw exportError\('viewer\.export\.error\.toBlobUnavailable'/);
assert.match(html, /img\.onload = function \(\) \{\s*try \{/);
assert.match(html, /function rasterizeShareCard\(options\)[\s\S]*?if \(!options\.variant\) return renderShareCard\(\);/);
assert.match(html, /function renderShareCard\(options\)[\s\S]*?serializeSvg\(sourceScale, \{ routeSnapshot: routeSnapshot, reachSnapshot: reachSnapshot \}\)/);
assert.match(html, /fitCanvasText\(ctx, title, [^)]+\)/);
assert.match(html, /ARCHIFY ·/);
assert.doesNotMatch(svgBlock(html), /share-card|Share Card|ARCHIFY ·/);
});
test('Share Card is a canonical PNG with exact receipt dimensions and filename', () => {
const html = render('workflow');
assert.match(html, /recordExportReceipt\('share-card', blob, true, \{ width: SHARE_CARD_WIDTH, height: SHARE_CARD_HEIGHT \}\)/);
assert.match(html, /base \+ '-share-card\.png'/);
assert.match(html, /data-last-export-width/);
assert.match(html, /data-last-export-height/);
assert.match(html, /shareCard: rasterizeShareCard/);
assert.match(html, /if \(format === 'share-card'\) return true;/);
});
test('Copy Share Card reuses one canonical card blob and writes only PNG to the clipboard', () => {
const html = render('architecture');
assert.match(html, /data-action="copy-share-card"/);
assert.match(html, /Copy Share Card[\s\S]*?<small class="hint">PNG to clipboard<\/small>/);
assert.match(html, /function runCopyShareCard\(\)[\s\S]*?var blobPromise = rasterizeShareCard\(\);/);
const copyBlock = html.match(/function runCopyShareCard\(\) \{[\s\S]*?\n \}/)?.[0] || '';
assert.equal((copyBlock.match(/rasterizeShareCard\(\)/g) || []).length, 1);
assert.match(copyBlock, /writePngToClipboard\(blobPromise\)/);
assert.match(html, /new ClipboardItem\(\{ 'image\/png': blobPromise \}\)/);
assert.match(copyBlock, /recordExportReceipt\('share-card', blob, true, \{ width: SHARE_CARD_WIDTH, height: SHARE_CARD_HEIGHT \}\)/);
assert.match(copyBlock, /toast\(viewerText\('viewer\.export\.copiedShare'\)\)/);
assert.match(html, /copyShareCard: runCopyShareCard/);
});
test('Copy Share Card fails closed when image clipboard writing is unavailable', () => {
const html = render('workflow');
assert.match(html, /it\.dataset\.action === 'copy-share-card'[\s\S]*?!canCopyImage\(\)/);
assert.match(html, /function runCopyShareCard\(\)[\s\S]*?if \(!canCopyImage\(\)\)/);
assert.match(html, /Clipboard image write not supported by this browser/);
assert.match(html, /button\[data-action="copy-share-card"\]/);
assert.match(html, /document\.documentElement\.removeAttribute\('data-last-export-format'\)/);
assert.match(html, /data-last-export-error-format', 'share-card'/);
});
test('ordinary Copy PNG keeps its existing full-diagram raster path', () => {
const html = render('sequence');
assert.match(html, /function runCopy\(\)[\s\S]*?var blobPromise = rasterize\('png'\);/);
assert.match(html, /runCopy\(\)[\s\S]*?writePngToClipboard\(blobPromise\)/);
assert.doesNotMatch(svgBlock(html), /copy-share-card|Copy Share Card/);
});
test('Share Card stays viewer-only and reuses export cleanup instead of source state', () => {
const html = render('sequence');
assert.match(html, /html\[data-embed="true"\] \.toolbar/);
assert.match(html, /@media print[\s\S]*?\.toolbar/);
assert.match(html, /function rasterizeShareCard\(options\)[\s\S]*?if \(!options\.variant\) return renderShareCard\(\);/);
assert.match(html, /function renderShareCard\(options\)[\s\S]*?serializeSvg\(sourceScale, \{ routeSnapshot: routeSnapshot, reachSnapshot: reachSnapshot \}\)/);
assert.match(html, /if \(!data\.canonicalStateClean\) return Promise\.reject\(exportError\('viewer\.export\.error\.viewerState'\)\);/);
assert.match(html, /canonicalStateClean/);
assert.doesNotMatch(svgBlock(html), /data-last-export-|data-format="share-card"/);
});
test('the skill and every README make the optional Share Card discoverable', () => {
const viewer = fs.readFileSync(path.join(skillRoot, 'references', 'viewer-runtime.md'), 'utf8');
assert.match(viewer, /optional 1200(?:×|x)630 Share Card PNG/i);
assert.match(viewer, /current theme and visual preset/i);
assert.match(viewer, /never claim(?:s|ing)? validation/i);
assert.match(viewer, /Copy Share Card/i);
for (const readme of ['README.md', 'README_EN.md', 'README_ZH.md']) {
const text = fs.readFileSync(path.join(repoRoot, readme), 'utf8');
assert.match(text, /Share Card/i, readme);
assert.match(text, /1200(?:×|x)630/, readme);
assert.match(text, /copy|复制/i, readme);
}
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,562 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import vm from 'node:vm';
import { fileURLToPath } from 'node:url';
import { ChromeVisualBrowser, findChrome } from '../bin/visual-check.mjs';
import { DIAGRAM_TYPES, DIAGRAM_TYPE_LABELS } from '../../scripts/site-copy.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '../..');
const runtimePath = path.join(repoRoot, 'docs/assets/site-language.js');
const navigationPath = path.join(repoRoot, 'docs/assets/site-navigation.css');
const integrationEnabled = process.env.ARCHIFY_SITE_INTEGRATION === '1';
const chromePath = integrationEnabled && process.env.ARCHIFY_CHROME ? findChrome() : null;
function loadRuntime({
url = 'https://example.test/',
values = new Map(),
storageError = false,
historyError = false,
source = runtimePath,
} = {}) {
const localStorage = {
getItem(key) {
if (storageError) throw new Error('storage unavailable');
return values.has(key) ? values.get(key) : null;
},
setItem(key, value) {
if (storageError) throw new Error('storage unavailable');
values.set(key, String(value));
},
};
let currentUrl = new URL(url);
const location = {};
function syncLocation() {
location.href = currentUrl.href;
location.search = currentUrl.search;
location.pathname = currentUrl.pathname;
location.hash = currentUrl.hash;
}
syncLocation();
const window = {
location,
history: {
replaceState(_state, _title, next) {
if (historyError) throw new Error('history unavailable');
currentUrl = new URL(next, currentUrl);
syncLocation();
},
},
localStorage,
};
vm.runInNewContext(fs.readFileSync(source, 'utf8'), { window, URL, URLSearchParams });
return { language: window.ArchifySiteLanguage, values, url: () => new URL(currentUrl) };
}
async function evaluate(browser, sessionId, expression) {
const response = await browser.cdp.send('Runtime.evaluate', {
expression,
awaitPromise: true,
returnByValue: true,
}, sessionId);
if (response.exceptionDetails) {
throw new Error(response.exceptionDetails.exception?.description
|| response.exceptionDetails.text
|| 'Runtime.evaluate failed');
}
return response.result?.value;
}
async function navigate(browser, sessionId, url) {
const loaded = browser.cdp.waitFor('Page.loadEventFired', sessionId);
const navigation = await browser.cdp.send('Page.navigate', { url }, sessionId);
if (navigation.errorText) throw new Error(`Chrome navigation failed: ${navigation.errorText}`);
await loaded;
}
async function clickAndNavigate(browser, sessionId, selector) {
const loaded = browser.cdp.waitFor('Page.loadEventFired', sessionId);
await evaluate(browser, sessionId, `(function () {
var link = document.querySelector(${JSON.stringify(selector)});
if (!link) throw new Error('Missing navigation link: ' + ${JSON.stringify(selector)});
link.click();
})()`);
await loaded;
}
function startStaticServer(root) {
const server = http.createServer((request, response) => {
const requestUrl = new URL(request.url || '/', 'http://127.0.0.1');
const relative = decodeURIComponent(requestUrl.pathname).replace(/^\/+/, '') || 'index.html';
const requestedPath = path.resolve(root, relative);
if (!requestedPath.startsWith(`${path.resolve(root)}${path.sep}`)) {
response.writeHead(403).end('Forbidden');
return;
}
try {
const body = fs.readFileSync(requestedPath);
const contentType = requestedPath.endsWith('.css') ? 'text/css'
: requestedPath.endsWith('.js') ? 'text/javascript'
: requestedPath.endsWith('.json') ? 'application/json'
: 'text/html';
response.writeHead(200, { 'content-type': `${contentType}; charset=utf-8` });
response.end(body);
} catch (_) {
response.writeHead(404).end('Not found');
}
});
return server;
}
test('site language runtime normalizes one entry parameter into one durable preference', () => {
const canonical = loadRuntime({ values: new Map([['archify-lang', 'zh']]) });
assert.equal(canonical.language.read(), 'zh');
for (const legacyKey of ['archify-gallery-language', 'archify-guide-language']) {
const legacy = loadRuntime({ values: new Map([[legacyKey, 'zh']]) });
assert.equal(legacy.language.read(), 'zh', `${legacyKey} must remain readable during migration`);
assert.equal(legacy.values.get('archify-lang'), 'zh', `${legacyKey} must migrate to the canonical key`);
}
const canonicalWins = loadRuntime({
values: new Map([
['archify-lang', 'en'],
['archify-gallery-language', 'zh'],
['archify-guide-language', 'zh'],
]),
});
assert.equal(canonicalWins.language.read(), 'en');
const secondLegacyFallback = loadRuntime({
values: new Map([
['archify-gallery-language', 'fr'],
['archify-guide-language', 'zh'],
]),
});
assert.equal(secondLegacyFallback.language.read(), 'zh');
assert.equal(secondLegacyFallback.values.get('archify-lang'), 'zh');
const conflictingLegacy = loadRuntime({
values: new Map([
['archify-gallery-language', 'zh'],
['archify-guide-language', 'en'],
]),
});
assert.equal(conflictingLegacy.language.read(), 'zh');
assert.equal(conflictingLegacy.values.get('archify-lang'), 'zh');
conflictingLegacy.values.set('archify-gallery-language', 'en');
const migrated = loadRuntime({ values: conflictingLegacy.values });
assert.equal(migrated.language.read(), 'zh', 'the canonical migration must win on later page loads');
const explicit = loadRuntime({
url: 'https://example.test/guide.html?lang=en&type=workflow#chooser',
values: new Map([['archify-lang', 'zh']]),
});
assert.equal(explicit.language.read(), 'en');
assert.equal(explicit.values.get('archify-lang'), 'en');
assert.equal(explicit.url().searchParams.has('lang'), false);
assert.equal(explicit.url().searchParams.get('type'), 'workflow');
assert.equal(explicit.url().hash, '#chooser');
const historyBlocked = loadRuntime({
url: 'https://example.test/guide.html?lang=zh&type=workflow#chooser',
values: new Map([['archify-lang', 'en']]),
historyError: true,
});
assert.equal(historyBlocked.language.read(), 'zh');
assert.equal(historyBlocked.values.get('archify-lang'), 'zh');
assert.equal(historyBlocked.url().searchParams.get('lang'), 'zh');
assert.equal(explicit.language.write('zh'), 'zh');
const refreshed = loadRuntime({ url: explicit.url().href, values: explicit.values });
assert.equal(refreshed.language.read(), 'zh');
const unsupported = loadRuntime({
url: 'https://example.test/?lang=fr',
values: new Map([['archify-lang', 'zh']]),
});
assert.equal(unsupported.language.read(), 'zh');
assert.equal(unsupported.url().searchParams.has('lang'), false);
const defaultLanguage = loadRuntime();
assert.equal(defaultLanguage.language.read(), 'en');
const blocked = loadRuntime({ storageError: true });
assert.equal(blocked.language.read(), 'en');
assert.equal(blocked.language.write('zh'), 'zh');
const source = fs.readFileSync(runtimePath, 'utf8');
assert.match(source, /archify-gallery-language/);
assert.match(source, /archify-guide-language/);
assert.doesNotMatch(source, /navigator\.language|detectBrowserLanguage|select\s*:/);
});
test('custom site builders emit every shared site asset and preserve entry, navigation, selection, and refresh state', {
skip: integrationEnabled ? false : 'Run through the serialized site integration gate.',
}, () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-site-language-'));
try {
const builds = [
{ script: 'build-start.mjs', args: [path.join(tmp, 'start-site/start.html')], root: 'start-site' },
{ script: 'build-guide.mjs', args: [path.join(tmp, 'guide-site/guide.html')], root: 'guide-site' },
{ script: 'build-gallery.mjs', args: [path.join(tmp, 'gallery-site')], root: 'gallery-site' },
];
for (const build of builds) {
execFileSync(process.execPath, [path.join(repoRoot, 'scripts', build.script), ...build.args]);
for (const asset of ['site-language.js', 'site-navigation.css']) {
const emitted = path.join(tmp, build.root, 'assets', asset);
const canonicalAsset = path.join(repoRoot, 'docs/assets', asset);
assert.ok(fs.existsSync(emitted), `${build.script}: ${asset} missing from custom output`);
assert.equal(fs.readFileSync(emitted, 'utf8'), fs.readFileSync(canonicalAsset, 'utf8'));
}
}
const emittedRuntime = path.join(tmp, 'start-site/assets/site-language.js');
const values = new Map();
const landing = loadRuntime({
url: 'https://example.test/?lang=zh&utm_source=readme#proof',
values,
source: emittedRuntime,
});
assert.equal(landing.language.read(), 'zh');
assert.equal(values.get('archify-lang'), 'zh');
assert.equal(landing.url().searchParams.has('lang'), false);
assert.equal(landing.url().searchParams.get('utm_source'), 'readme');
assert.equal(landing.url().hash, '#proof');
for (const page of ['gallery.html', 'guide.html', 'start.html']) {
const navigation = loadRuntime({ url: `https://example.test/${page}`, values, source: emittedRuntime });
assert.equal(navigation.language.read(), 'zh', page);
}
const explicitEnglish = loadRuntime({
url: 'https://example.test/guide.html?lang=en#recipes',
values,
source: emittedRuntime,
});
assert.equal(explicitEnglish.language.read(), 'en');
assert.equal(values.get('archify-lang'), 'en');
assert.equal(explicitEnglish.url().searchParams.has('lang'), false);
explicitEnglish.language.write('zh');
assert.equal(explicitEnglish.url().searchParams.has('lang'), false);
assert.equal(explicitEnglish.url().hash, '#recipes');
const refreshed = loadRuntime({ url: explicitEnglish.url().href, values, source: emittedRuntime });
assert.equal(refreshed.language.read(), 'zh');
const nextPage = loadRuntime({ url: 'https://example.test/gallery.html', values, source: emittedRuntime });
assert.equal(nextPage.language.read(), 'zh');
nextPage.language.write('en');
const refreshedEnglish = loadRuntime({ url: nextPage.url().href, values, source: emittedRuntime });
assert.equal(refreshedEnglish.language.read(), 'en');
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test('all site pages consume one language runtime and one navigation contract', () => {
const pages = [
'docs/index.html',
'scripts/gallery-template.html',
'scripts/guide-template.html',
'scripts/start-template.html',
'docs/gallery.html',
'docs/guide.html',
'docs/start.html',
];
for (const relative of pages) {
const html = fs.readFileSync(path.join(repoRoot, relative), 'utf8');
assert.match(html, /<script src="assets\/site-language\.js"><\/script>/, `${relative}: shared runtime missing`);
assert.match(html, /<link rel="stylesheet" href="assets\/site-navigation\.css">/, `${relative}: shared navigation missing`);
assert.match(html, /<nav class="site-nav" aria-label="Primary navigation">/, `${relative}: canonical navigation root missing`);
assert.match(html, /ArchifySiteLanguage\.read\(/, `${relative}: shared language read missing`);
assert.match(html, /ArchifySiteLanguage\.write\(/, `${relative}: shared language write missing`);
assert.match(html, /href="guide\.html"/, `${relative}: Guide navigation missing`);
assert.match(html, /href="gallery\.html"/, `${relative}: Proof Lab navigation missing`);
assert.match(html, /href="start\.html"/, `${relative}: Start navigation missing`);
assert.match(html, /class="btn btn-primary nav-cta"/, `${relative}: install action missing`);
assert.doesNotMatch(html, /(?:^|\s)nav\s*\{/, `${relative}: inline navigation layout bypasses the shared contract`);
assert.doesNotMatch(html, /\.nav-right\s*\{/, `${relative}: inline navigation actions bypass the shared contract`);
assert.doesNotMatch(
html,
/localStorage\.setItem\(['"]archify-(?:lang|gallery-language|guide-language)['"]/,
`${relative}: page bypasses the shared language writer`,
);
}
const navigation = fs.readFileSync(navigationPath, 'utf8');
assert.match(navigation, /\.site-nav\s*\{/);
assert.match(navigation, /\.site-nav \.nav-right\s*\{/);
assert.match(navigation, /@media \(max-width: 640px\)/);
});
test('site page identity paths localize with the selected language', () => {
const pages = [
{ paths: ['scripts/guide-template.html', 'docs/guide.html'], en: '/ guide', zh: '/ 场景指南' },
{ paths: ['scripts/gallery-template.html', 'docs/gallery.html'], en: '/ proof lab', zh: '/ 验证作品集' },
{ paths: ['scripts/start-template.html', 'docs/start.html'], en: '/ start', zh: '/ 快速上手' },
];
for (const page of pages) {
for (const relative of page.paths) {
const html = fs.readFileSync(path.join(repoRoot, relative), 'utf8');
assert.ok(
html.includes(`<span class="nav-logo-path" data-en="${page.en}" data-zh="${page.zh}">${page.en}</span>`),
`${relative}: page identity path must expose matching English and Chinese copy`,
);
assert.match(
html,
/querySelectorAll\('\[data-en\]\[data-zh\]'\)/,
`${relative}: language changes must update bilingual page identity copy`,
);
}
}
});
test('proof gallery type filters localize with the selected language', () => {
const filters = DIAGRAM_TYPES.map((type) => ({
type,
en: DIAGRAM_TYPE_LABELS.en[type],
zh: DIAGRAM_TYPE_LABELS.zh[type],
}));
const template = fs.readFileSync(path.join(repoRoot, 'scripts/gallery-template.html'), 'utf8');
for (const filter of filters) {
const placeholder = filter.type.toUpperCase();
assert.ok(
template.includes(`data-filter="${filter.type}" aria-pressed="false" data-en="[[DIAGRAM_TYPE_${placeholder}_EN]]" data-zh="[[DIAGRAM_TYPE_${placeholder}_ZH]]"`),
`gallery template: ${filter.type} filter must consume the shared copy source`,
);
}
for (const relative of ['docs/gallery.html']) {
const html = fs.readFileSync(path.join(repoRoot, relative), 'utf8');
assert.match(
html,
/<button(?=[^>]*data-filter="all")(?=[^>]*data-en="All \/ [^"]+")(?=[^>]*data-zh="全部配方 \/ [^"]+")[^>]*>All \/ [^<]+<\/button>/,
`${relative}: all filter must expose English and Chinese copy`,
);
for (const filter of filters) {
const bilingualFilter = new RegExp(
`<button(?=[^>]*data-filter="${filter.type}")(?=[^>]*data-en="${filter.en}")(?=[^>]*data-zh="${filter.zh}")[^>]*>${filter.en}<\\/button>`,
);
assert.match(html, bilingualFilter, `${relative}: ${filter.type} filter must expose English and Chinese copy`);
}
assert.match(
html,
/querySelectorAll\('\[data-en\]\[data-zh\]'\)/,
`${relative}: language changes must update bilingual gallery filters`,
);
}
});
test('scenario guide type filters use consistent Chinese diagram names', () => {
const template = fs.readFileSync(path.join(repoRoot, 'scripts/guide-template.html'), 'utf8');
assert.match(template, /var types = \[\[DIAGRAM_TYPES_JSON\]\];/);
assert.match(template, /var labels = \[\[DIAGRAM_TYPE_LABELS_JSON\]\];/);
const html = fs.readFileSync(path.join(repoRoot, 'docs/guide.html'), 'utf8');
assert.ok(
html.includes(`var labels = ${JSON.stringify(DIAGRAM_TYPE_LABELS)};`),
'docs/guide.html: Guide filters must use the shared Chinese diagram names',
);
});
test('real Chrome preserves language through entry, navigation, selection, refresh, and consistent navigation chrome', {
skip: chromePath ? false : 'Set ARCHIFY_CHROME to run the real site regression.',
timeout: 60000,
}, async () => {
const docsRoot = path.join(repoRoot, 'docs');
const server = startStaticServer(docsRoot);
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
const baseUrl = `http://127.0.0.1:${address.port}`;
const browser = new ChromeVisualBrowser(chromePath);
try {
const sessionId = await browser.sessionPromise;
await browser.cdp.send('Emulation.setDeviceMetricsOverride', {
width: 1440,
height: 900,
deviceScaleFactor: 1,
mobile: false,
}, sessionId);
await navigate(browser, sessionId, `${baseUrl}/index.html`);
await evaluate(browser, sessionId, 'localStorage.clear()');
await evaluate(browser, sessionId, "localStorage.setItem('archify-guide-language', 'zh')");
await navigate(browser, sessionId, `${baseUrl}/index.html`);
assert.deepEqual(await evaluate(browser, sessionId, `({
language: document.documentElement.lang,
stored: localStorage.getItem('archify-lang')
})`), { language: 'zh-CN', stored: 'zh' });
await evaluate(browser, sessionId, 'localStorage.clear()');
await navigate(browser, sessionId, `${baseUrl}/index.html?lang=zh&utm_source=browser-test#proof`);
let state = await evaluate(browser, sessionId, `({
language: document.documentElement.lang,
stored: localStorage.getItem('archify-lang'),
langQuery: new URL(location.href).searchParams.get('lang'),
campaign: new URL(location.href).searchParams.get('utm_source'),
hash: location.hash
})`);
assert.deepEqual(state, {
language: 'zh-CN', stored: 'zh', langQuery: null, campaign: 'browser-test', hash: '#proof',
});
await clickAndNavigate(browser, sessionId, '.site-nav a[href="gallery.html"]');
assert.equal(await evaluate(browser, sessionId, 'document.documentElement.lang'), 'zh-CN');
assert.equal(await evaluate(browser, sessionId, 'document.querySelector(".nav-logo-path").textContent'), '/ 验证作品集');
assert.deepEqual(await evaluate(browser, sessionId, `Array.from(document.querySelectorAll('[data-filter]')).map(function (button) {
return button.textContent;
})`), ['全部配方 / 11', '架构图', '工作流', '时序图', '数据流', '生命周期']);
await evaluate(browser, sessionId, 'document.querySelector(\'[data-filter="architecture"]\').click()');
state = await evaluate(browser, sessionId, `({
language: document.documentElement.lang,
selected: document.querySelector('[data-filter="architecture"]').getAttribute('aria-pressed'),
typeQuery: new URL(location.href).searchParams.get('type'),
visibleCount: document.querySelectorAll('.showcase-card:not([hidden])').length,
onlyArchitecture: Array.from(document.querySelectorAll('.showcase-card:not([hidden])')).every(function (card) {
return card.getAttribute('data-type') === 'architecture';
})
})`);
assert.deepEqual(state, {
language: 'zh-CN', selected: 'true', typeQuery: 'architecture', visibleCount: 2, onlyArchitecture: true,
});
let loaded = browser.cdp.waitFor('Page.loadEventFired', sessionId);
await browser.cdp.send('Page.reload', {}, sessionId);
await loaded;
assert.deepEqual(await evaluate(browser, sessionId, `({
language: document.documentElement.lang,
selected: document.querySelector('[data-filter="architecture"]').getAttribute('aria-pressed'),
visibleCount: document.querySelectorAll('.showcase-card:not([hidden])').length
})`), { language: 'zh-CN', selected: 'true', visibleCount: 2 });
await evaluate(browser, sessionId, 'document.getElementById("language").click()');
assert.equal(await evaluate(browser, sessionId, 'document.documentElement.lang'), 'en');
assert.equal(await evaluate(browser, sessionId, 'document.querySelector(".nav-logo-path").textContent'), '/ proof lab');
assert.deepEqual(await evaluate(browser, sessionId, `Array.from(document.querySelectorAll('[data-filter]')).map(function (button) {
return button.textContent;
})`), ['All / 11', 'Architecture', 'Workflow', 'Sequence', 'Data flow', 'Lifecycle']);
loaded = browser.cdp.waitFor('Page.loadEventFired', sessionId);
await browser.cdp.send('Page.reload', {}, sessionId);
await loaded;
assert.equal(await evaluate(browser, sessionId, 'document.documentElement.lang'), 'en');
await clickAndNavigate(browser, sessionId, '.site-nav a[href="guide.html"]');
assert.equal(await evaluate(browser, sessionId, 'document.documentElement.lang'), 'en');
await navigate(browser, sessionId, `${baseUrl}/guide.html?lang=zh#recipes`);
state = await evaluate(browser, sessionId, `({
language: document.documentElement.lang,
stored: localStorage.getItem('archify-lang'),
langQuery: new URL(location.href).searchParams.get('lang'),
hash: location.hash
})`);
assert.deepEqual(state, { language: 'zh-CN', stored: 'zh', langQuery: null, hash: '#recipes' });
assert.equal(await evaluate(browser, sessionId, 'document.querySelector(".nav-logo-path").textContent'), '/ 场景指南');
assert.deepEqual(await evaluate(browser, sessionId, `Array.from(document.querySelectorAll('#filters [data-filter]')).map(function (button) {
return button.textContent;
})`), ['全部配方', '架构图', '工作流', '时序图', '数据流', '生命周期']);
await evaluate(browser, sessionId, 'document.querySelector(\'#filters [data-filter="sequence"]\').click()');
state = await evaluate(browser, sessionId, `({
language: document.documentElement.lang,
selected: document.querySelector('#filters [data-filter="sequence"]').classList.contains('active'),
visibleCount: document.querySelectorAll('#cards .card').length,
onlySequence: Array.from(document.querySelectorAll('#cards .card .card-type')).every(function (label) {
return label.textContent === 'sequence';
}),
labels: Array.from(document.querySelectorAll('#filters [data-filter]')).map(function (button) {
return button.textContent;
})
})`);
assert.deepEqual(state, {
language: 'zh-CN',
selected: true,
visibleCount: 2,
onlySequence: true,
labels: ['全部配方', '架构图', '工作流', '时序图', '数据流', '生命周期'],
});
await clickAndNavigate(browser, sessionId, '.site-nav a[href="start.html"]');
assert.equal(await evaluate(browser, sessionId, 'document.documentElement.lang'), 'zh-CN');
assert.equal(await evaluate(browser, sessionId, 'new URL(location.href).searchParams.has("lang")'), false);
assert.equal(await evaluate(browser, sessionId, 'document.querySelector(".nav-logo-path").textContent'), '/ 快速上手');
const pages = ['index.html', 'gallery.html', 'guide.html', 'start.html'];
const desktopReceipts = [];
for (const page of pages) {
await navigate(browser, sessionId, `${baseUrl}/${page}`);
desktopReceipts.push(await evaluate(browser, sessionId, `(function () {
var nav = document.querySelector('.site-nav');
var logo = nav.querySelector('.nav-logo-text');
var actions = nav.querySelector('.nav-right');
var language = nav.querySelector('.btn-lang');
var cta = nav.querySelector('.nav-cta');
var navStyle = getComputedStyle(nav);
var logoStyle = getComputedStyle(logo);
var actionsStyle = getComputedStyle(actions);
var languageStyle = getComputedStyle(language);
var ctaStyle = getComputedStyle(cta);
return {
height: nav.getBoundingClientRect().height,
position: navStyle.position,
paddingLeft: navStyle.paddingLeft,
background: navStyle.backgroundColor,
borderBottom: navStyle.borderBottomWidth + ' ' + navStyle.borderBottomStyle + ' ' + navStyle.borderBottomColor,
logoFont: logoStyle.fontFamily,
logoSize: logoStyle.fontSize,
actionGap: actionsStyle.gap,
languageHeight: language.getBoundingClientRect().height,
languageRadius: languageStyle.borderRadius,
ctaHeight: cta.getBoundingClientRect().height,
ctaRadius: ctaStyle.borderRadius,
linkCount: nav.querySelectorAll('.nav-link').length
};
})()`));
}
for (const receipt of desktopReceipts.slice(1)) assert.deepEqual(receipt, desktopReceipts[0]);
await browser.cdp.send('Emulation.setDeviceMetricsOverride', {
width: 390,
height: 844,
deviceScaleFactor: 1,
mobile: true,
}, sessionId);
for (const page of pages) {
await navigate(browser, sessionId, `${baseUrl}/${page}`);
const mobile = await evaluate(browser, sessionId, `(function () {
var nav = document.querySelector('.site-nav');
var rect = nav.getBoundingClientRect();
var actions = nav.querySelector('.nav-right').getBoundingClientRect();
return {
height: rect.height,
left: rect.left,
right: rect.right,
actionsRight: actions.right,
linkDisplay: getComputedStyle(nav.querySelector('.nav-link')).display
};
})()`);
assert.deepEqual(mobile, { height: 60, left: 0, right: 390, actionsRight: 370, linkDisplay: 'none' }, page);
}
} finally {
await browser.close();
await new Promise((resolve) => server.close(resolve));
}
});
@@ -0,0 +1,9 @@
process.env.ARCHIFY_SITE_INTEGRATION = '1';
// GitHub-hosted Linux runners require the same explicit Chrome sandbox opt-out
// already used by this workflow's other real-browser regression steps.
if (process.platform === 'linux' && process.env.GITHUB_ACTIONS === 'true') {
process.env.ARCHIFY_CHROME_NO_SANDBOX = '1';
}
await import('./site-language-continuity.test.mjs');
@@ -0,0 +1,84 @@
import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import assert from 'node:assert/strict';
const here = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.join(here, '..');
const skill = readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8');
const authoringContract = readFileSync(path.join(skillRoot, 'references', 'authoring-contract.md'), 'utf8');
const frontmatter = skill.match(/^---\n([\s\S]*?)\n---/);
test('skill description is portable across 1024-character runtimes and remains searchable', () => {
assert.ok(frontmatter, 'SKILL.md must start with YAML frontmatter');
const description = frontmatter[1].match(/^description:\s*(.+)$/m)?.[1]?.trim();
assert.ok(description, 'frontmatter must include a one-line description');
assert.ok(description.length <= 1024, `description is ${description.length} characters; maximum is 1024`);
assert.ok(Buffer.byteLength(description, 'utf8') <= 1024, 'description must also fit a 1024-byte runtime limit');
for (const trigger of ['architecture', 'workflow', 'sequence', 'data-flow', 'lifecycle', 'Mermaid']) {
assert.match(description, new RegExp(`\\b${trigger}\\b`, 'i'), `description must retain the ${trigger} trigger`);
}
assert.match(description, /standalone HTML/i);
assert.match(description, /Use when/i);
});
test('literal packaged-skill path references resolve inside the installed skill root', () => {
const references = [...skill.matchAll(/`((?:assets|bin|examples|recipes|references|renderers|schemas|scripts)\/[^`\s]+)`/g)]
.map((match) => match[1])
.filter((reference) => !/[<>{}*\[\]]/.test(reference));
assert.ok(references.length > 0, 'expected literal packaged-skill references');
for (const reference of new Set(references)) {
assert.equal(existsSync(path.join(skillRoot, reference)), true, `SKILL.md references missing packaged path ${reference}`);
}
});
test('main skill stays a bounded authoring router with progressive references', () => {
const lines = skill.trimEnd().split('\n');
assert.ok(lines.length <= 160, `SKILL.md is ${lines.length} lines; keep the entrypoint at 160 or fewer`);
for (const reference of [
'references/authoring-contract.md',
'references/viewer-runtime.md',
'references/delivery-contract.md',
]) {
assert.match(skill, new RegExp(reference.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
assert.equal(existsSync(path.join(skillRoot, reference)), true, `${reference} must ship with the skill`);
}
});
test('update awareness is notification-only and never replaces the requested workflow', () => {
assert.match(skill, /`scripts\/check-update\.mjs`/);
assert.match(skill, /`silent`[\s\S]*without mentioning/i);
assert.match(skill, /`update_available`[\s\S]*compact notice/i);
assert.match(skill, /information, not permission/i);
assert.match(skill, /`severity` is `security`[\s\S]*security update[\s\S]*emphasis only, never user autonomy/i);
assert.match(skill, /continue the user's original task/i);
assert.match(skill, /installed version unchanged/i);
assert.doesNotMatch(skill, /npx skills update|gh skill update/i);
});
test('language behavior stays within the bounded locale contract', () => {
assert.match(skill, /one primary authored language/);
assert.match(skill, /explicit user choice; otherwise follow the request or conversation's dominant language/);
assert.match(skill, /`meta\.locale` controls only renderer-owned Viewer UI/);
assert.match(skill, /use `"en"` or `"zh-CN"`/);
assert.match(skill, /For every other language, omit `meta\.locale`/);
assert.match(skill, /fixed Viewer UI and `<html lang>` fall back to English/);
assert.match(skill, /renderer never translates authored content/i);
assert.match(skill, /product names.*code identifiers.*protocols.*API paths.*environment names/);
assert.match(authoringContract, /`meta\.locale` controls only renderer-owned reader surfaces/);
assert.match(authoringContract, /outside `en` and `zh-CN`/);
assert.match(authoringContract, /artifact is\s+not fully localized/);
assert.match(authoringContract, /Do not silently substitute\s+`zh-CN` for another language or Chinese locale/);
assert.match(authoringContract, /It never translates authored content/);
assert.match(authoringContract, /Renderer-owned default legend labels follow `meta\.locale`/);
assert.match(authoringContract, /The fallback\s+applies only to renderer-owned surfaces/);
});
test('skill keeps the title hierarchy compact by default', () => {
assert.match(skill, /Omit `meta\.subtitle` by default/);
assert.match(skill, /Never invent a subtitle that restates the title, nodes, or cards/);
assert.match(authoringContract, /omitted or blank subtitle must not leave an empty visual row/);
});
@@ -0,0 +1,167 @@
import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, '..', '..');
const checker = path.join(repoRoot, 'scripts', 'check-stable-update-manifest.mjs');
function git(root, args) {
return spawnSync('git', args, { cwd: root, encoding: 'utf8' });
}
function annotatedTaggerTime(root, tag) {
const result = git(root, [
'for-each-ref',
'--format=%(taggerdate:unix)',
`refs/tags/${tag}`,
]);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout.trim(), /^\d+$/);
return new Date(Number(result.stdout.trim()) * 1_000)
.toISOString()
.replace('.000Z', 'Z');
}
function writeJson(target, value) {
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`);
}
function runCheck(root, archive, extraArguments = []) {
return spawnSync(process.execPath, [
checker,
'--root', root,
'--archive', archive,
'--tag', 'v3.0.0',
...extraArguments,
], { encoding: 'utf8' });
}
test('stable release gate binds manifest tag, tree, and final archive digest', () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-stable-manifest-'));
try {
writeJson(path.join(fixture, 'archify', 'package.json'), { version: '3.0.0' });
fs.writeFileSync(path.join(fixture, 'archify', 'SKILL.md'), 'stable fixture\n');
assert.equal(git(fixture, ['init']).status, 0);
assert.equal(git(fixture, ['add', 'archify']).status, 0);
assert.equal(git(fixture, [
'-c', 'user.name=Archify Test',
'-c', 'user.email=archify@example.invalid',
'commit', '-m', 'stable fixture',
]).status, 0);
assert.equal(git(fixture, [
'-c', 'user.name=Archify Test',
'-c', 'user.email=archify@example.invalid',
'tag', '-a', 'v3.0.0', '-m', 'stable v3.0.0',
]).status, 0);
const publishedAt = annotatedTaggerTime(fixture, 'v3.0.0');
const tree = git(fixture, ['rev-parse', 'HEAD:archify']);
assert.equal(tree.status, 0, tree.stderr);
const treeSha = tree.stdout.trim();
const archive = path.join(fixture, 'archify.zip');
const archiveBytes = Buffer.from('deterministic stable archive fixture');
fs.writeFileSync(archive, archiveBytes);
const artifactSha = crypto.createHash('sha256').update(archiveBytes).digest('hex');
const manifestPath = path.join(fixture, 'docs', 'skill-updates', 'archify', 'stable.json');
const manifest = {
schemaVersion: 1,
skillId: 'archify',
channel: 'stable',
version: '3.0.0',
publishedAt,
source: {
repository: 'https://github.com/tt-a1i/archify',
ref: 'v3.0.0',
treeSha,
},
artifact: { sha256: artifactSha },
summary: 'Stable release fixture.',
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v3.0.0',
severity: 'normal',
};
writeJson(manifestPath, manifest);
const passing = runCheck(fixture, archive);
assert.equal(passing.status, 0, passing.stderr);
assert.match(passing.stdout, /stable update manifest ok: v3\.0\.0/);
writeJson(path.join(fixture, 'archify', 'package.json'), { version: '4.0.0-dev.0' });
const historicalRelease = runCheck(fixture, archive, ['--source-ref', 'v3.0.0']);
assert.equal(historicalRelease.status, 0, historicalRelease.stderr);
const wrongHistoricalRef = runCheck(fixture, archive, ['--source-ref', 'v2.9.0']);
assert.notEqual(wrongHistoricalRef.status, 0);
assert.match(wrongHistoricalRef.stderr, /--source-ref must be HEAD or the exact release tag/);
writeJson(path.join(fixture, 'archify', 'package.json'), { version: '3.0.0' });
writeJson(manifestPath, {
...manifest,
publishedAt: '2026-08-28T08:00:00+08:00',
});
const nonUtcTimestamp = runCheck(fixture, archive);
assert.notEqual(nonUtcTimestamp.status, 0);
assert.match(nonUtcTimestamp.stderr, /stable update manifest identity/);
writeJson(manifestPath, manifest);
writeJson(manifestPath, {
...manifest,
publishedAt: new Date(Date.parse(publishedAt) + 1_000)
.toISOString()
.replace('.000Z', 'Z'),
});
const wrongTaggerTime = runCheck(fixture, archive);
assert.notEqual(wrongTaggerTime.status, 0);
assert.match(wrongTaggerTime.stderr, /publishedAt .* annotated tagger time/);
writeJson(manifestPath, manifest);
const invalidContracts = [
{ ...manifest, extra: true },
{ ...manifest, source: { ...manifest.source, extra: true } },
{ ...manifest, artifact: { ...manifest.artifact, extra: true } },
{ ...manifest, summary: '\u202eunsafe' },
{ ...manifest, severity: 'urgent' },
];
for (const invalid of invalidContracts) {
writeJson(manifestPath, invalid);
const rejected = runCheck(fixture, archive);
assert.notEqual(rejected.status, 0);
assert.match(rejected.stderr, /stable update manifest identity/);
}
writeJson(manifestPath, manifest);
writeJson(manifestPath, null);
const nullManifest = runCheck(fixture, archive);
assert.notEqual(nullManifest.status, 0);
assert.match(nullManifest.stderr, /stable update manifest identity/);
assert.doesNotMatch(nullManifest.stderr, /TypeError|check-stable-update-manifest\.mjs:\d+/);
writeJson(manifestPath, manifest);
fs.appendFileSync(archive, 'tampered');
const archiveMismatch = runCheck(fixture, archive);
assert.notEqual(archiveMismatch.status, 0);
assert.match(archiveMismatch.stderr, /archive sha256 .* does not match/);
fs.writeFileSync(archive, archiveBytes);
writeJson(manifestPath, {
...manifest,
source: { ...manifest.source, treeSha: 'c'.repeat(40) },
});
const treeMismatch = runCheck(fixture, archive);
assert.notEqual(treeMismatch.status, 0);
assert.match(treeMismatch.stderr, /treeSha .* does not match HEAD:archify/);
writeJson(manifestPath, manifest);
assert.equal(git(fixture, ['tag', '-d', 'v3.0.0']).status, 0);
assert.equal(git(fixture, ['tag', 'v3.0.0']).status, 0);
const lightweightTag = runCheck(fixture, archive);
assert.notEqual(lightweightTag.status, 0);
assert.match(lightweightTag.stderr, /must be an annotated tag/);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
@@ -0,0 +1,312 @@
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 vm from 'node:vm';
import { fileURLToPath } from 'node:url';
import { SCENARIO_RECIPES, startPromptsFor } from '../recipes/scenarios.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(skillRoot, '..');
class FakeElement {
constructor({ id = '', textContent = '', dataset = {} } = {}) {
this.id = id;
this.textContent = textContent;
this.dataset = dataset;
this.style = {};
this.attributes = {};
this.listeners = {};
this.tabIndex = 0;
}
setAttribute(name, value) { this.attributes[name] = String(value); }
getAttribute(name) { return this.attributes[name]; }
addEventListener(name, listener) { this.listeners[name] = listener; }
replaceChildren(...children) { this.children = children; }
appendChild() {}
remove() {}
select() {}
focus() { this.focused = true; }
click() { return this.listeners.click?.({ preventDefault() {} }); }
dispatchKey(key) { return this.listeners.keydown?.({ key, preventDefault() {} }); }
}
function executeStartPage(html) {
const dataMatch = html.match(/<script id="start-data" type="application\/json">([\s\S]*?)<\/script>/);
const scriptMatch = html.match(/<script>\n([\s\S]*?)\n <\/script>\n<\/body>/);
assert.ok(dataMatch);
assert.ok(scriptMatch);
const ids = Object.fromEntries([
'recipe-title', 'recipe-question', 'recipe-prompt', 'include-list', 'proof-link',
'proof-meta', 'copy-status', 'language', 'agent-state', 'install-command',
'project-command', 'copy-prompt', 'copy-starter',
].map((id) => [id, new FakeElement({ id })]));
ids['start-data'] = new FakeElement({ id: 'start-data', textContent: dataMatch[1] });
const types = ['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']
.map((type) => new FakeElement({ dataset: { type } }));
const agents = ['cursor', 'codex', 'claude-code', 'opencode']
.map((agent) => new FakeElement({ textContent: agent === 'codex' ? 'Codex' : agent, dataset: { agent } }));
const inputs = ['description', 'repository']
.map((input) => new FakeElement({ dataset: { input } }));
const copySources = ['install-command', 'project-command']
.map((copySource) => new FakeElement({ dataset: { copySource } }));
const copied = [];
const stored = new Map();
let replacedUrl = '';
const document = {
documentElement: {},
body: { appendChild() {} },
getElementById(id) { return ids[id]; },
createElement() { return new FakeElement(); },
execCommand() { return true; },
querySelector(selector) {
const match = selector.match(/^\[data-agent="([^"]+)"\]$/);
return match ? agents.find((element) => element.dataset.agent === match[1]) : null;
},
querySelectorAll(selector) {
if (selector === '[data-type]') return types;
if (selector === '[data-agent]') return agents;
if (selector === '[data-input]') return inputs;
if (selector === '[data-copy-source]') return copySources;
if (selector === '[data-en][data-zh]') return [];
return [];
},
};
const window = {
location: { href: 'https://example.test/start.html', search: '', pathname: '/start.html' },
isSecureContext: true,
dispatchEvent() {},
ArchifySiteLanguage: {
read() { return 'en'; },
write(value) { return value; },
},
};
const context = {
window,
ArchifySiteLanguage: window.ArchifySiteLanguage,
document,
navigator: { languages: ['en'], language: 'en', clipboard: { async writeText(value) { copied.push(value); } } },
history: { replaceState(_state, _title, url) { replacedUrl = url; } },
sessionStorage: {
getItem(key) { return stored.get(key) ?? null; },
setItem(key, value) { stored.set(key, value); },
},
CustomEvent: class { constructor(name, options) { this.name = name; this.detail = options.detail; } },
URL,
URLSearchParams,
Set,
Array,
JSON,
encodeURIComponent,
};
vm.createContext(context);
new vm.Script(scriptMatch[1]).runInContext(context);
return { data: JSON.parse(dataMatch[1]), ids, inputs, copied, window, getUrl: () => replacedUrl };
}
test('start page: checked-in HTML is reproducible from canonical scenario recipes', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-start-page-'));
const generated = path.join(tmp, 'start.html');
try {
execFileSync(process.execPath, [path.join(repoRoot, 'scripts/build-start.mjs'), generated]);
assert.equal(
fs.readFileSync(generated, 'utf8'),
fs.readFileSync(path.join(repoRoot, 'docs/start.html'), 'utf8'),
);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test('start page: offers five bounded bilingual starts without ingesting source content', () => {
const html = fs.readFileSync(path.join(repoRoot, 'docs/start.html'), 'utf8');
assert.doesNotMatch(html, /\[\[[A-Z0-9_]+\]\]/);
assert.match(html, /npx -y skills add tt-a1i\/archify --skill archify --agent codex --global --copy --yes/);
assert.match(html, /npx -y skills add tt-a1i\/archify --skill archify --agent codex --copy --yes/);
for (const agent of ['cursor', 'codex', 'claude-code', 'opencode']) {
assert.match(html, new RegExp(`role="tab" data-agent="${agent}"`));
}
assert.match(html, /data-en="Describe it\."/);
assert.match(html, /data-en="Archify maps it\."/);
assert.match(html, /data-zh="直接说,"/);
assert.match(html, /data-zh="Archify 就能画。"/);
assert.match(html, /id="copy-starter"/);
assert.match(html, /data-en="Copy install \+ prompt"/);
assert.match(html, /data-zh="复制安装命令 \+ 提示词"/);
assert.match(html, /data-en="No repository is required\./);
assert.match(html, /data-zh="不需要绑定代码库。/);
assert.match(html, /data-input="description"/);
assert.match(html, /data-input="repository"/);
const dataMatch = html.match(/<script id="start-data" type="application\/json">([\s\S]*?)<\/script>/);
assert.ok(dataMatch);
const data = JSON.parse(dataMatch[1]);
assert.deepEqual(Object.keys(data), ['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']);
assert.ok(Object.values(data).every((entry) => entry.en.prompt && entry.zh.prompt && entry.en.descriptionPrompt && entry.zh.descriptionPrompt && entry.en.repositoryPrompt && entry.zh.repositoryPrompt && entry.proof));
const scriptMatch = html.match(/<script>\n([\s\S]*?)\n <\/script>\n<\/body>/);
assert.ok(scriptMatch);
assert.doesNotThrow(() => new vm.Script(scriptMatch[1]));
assert.match(scriptMatch[1], /KNOWN_TYPES\.has\(requestedType\)/);
assert.match(scriptMatch[1], /KNOWN_AGENTS\.has\(requestedAgent\)/);
assert.match(scriptMatch[1], /KNOWN_SOURCES\.has\(requestedSource\)/);
assert.match(scriptMatch[1], /KNOWN_INPUTS\.has\(requestedInput\)/);
assert.match(scriptMatch[1], /next\.searchParams\.set\('agent', agent\)/);
assert.match(scriptMatch[1], /next\.searchParams\.set\('source', source\)/);
assert.match(scriptMatch[1], /next\.searchParams\.set\('input', input\)/);
assert.match(scriptMatch[1], /next\.searchParams\.delete\('lang'\)/);
assert.match(scriptMatch[1], /--agent ' \+ agent \+ ' --global --copy --yes/);
assert.match(scriptMatch[1], /--agent ' \+ agent \+ ' --copy --yes/);
assert.match(scriptMatch[1], /function starterText\(\)/);
assert.match(scriptMatch[1], /archify:start-funnel/);
assert.match(scriptMatch[1], /archify\.start\.events\.v1/);
assert.doesNotMatch(scriptMatch[1], /fetch\(|sendBeacon\(|XMLHttpRequest/);
assert.match(scriptMatch[1], /textContent/);
assert.match(scriptMatch[1], /replaceChildren/);
assert.doesNotMatch(scriptMatch[1], /innerHTML/);
});
test('start page: canonical recipes own description and repository prompt variants', () => {
const selected = new Map([
['architecture', 'system-overview'],
['workflow', 'agent-tool-call'],
['sequence', 'api-request'],
['dataflow', 'event-stream'],
['lifecycle', 'object-lifecycle'],
]);
for (const [type, id] of selected) {
const recipe = SCENARIO_RECIPES.find((candidate) => candidate.id === id);
assert.equal(recipe?.type, type);
for (const language of ['en', 'zh']) {
const prompts = startPromptsFor(recipe, language);
assert.equal(prompts.descriptionPrompt, recipe.start[language].descriptionPrompt);
assert.ok(prompts.repositoryPrompt.toLowerCase().includes(recipe[language].prompt.toLowerCase()));
}
}
});
test('start page: input mode drives rendered prompt, copy, keyboard, and URL without changing event schema', async () => {
const html = fs.readFileSync(path.join(repoRoot, 'docs/start.html'), 'utf8');
const page = executeStartPage(html);
const descriptionPrompt = page.data.architecture.en.descriptionPrompt;
const repositoryPrompt = page.data.architecture.en.repositoryPrompt;
assert.equal(page.inputs[0].getAttribute('aria-selected'), 'true');
assert.equal(page.inputs[1].getAttribute('aria-selected'), 'false');
assert.equal(page.ids['recipe-prompt'].textContent, descriptionPrompt);
assert.equal(new URL(page.getUrl(), 'https://example.test').searchParams.get('input'), 'description');
page.inputs[1].click();
assert.equal(page.inputs[1].getAttribute('aria-selected'), 'true');
assert.equal(page.ids['recipe-prompt'].textContent, repositoryPrompt);
assert.equal(new URL(page.getUrl(), 'https://example.test').searchParams.get('input'), 'repository');
page.ids['copy-prompt'].click();
await new Promise((resolve) => setImmediate(resolve));
assert.equal(page.copied.at(-1), repositoryPrompt);
page.inputs[1].dispatchKey('ArrowLeft');
assert.equal(page.inputs[0].getAttribute('aria-selected'), 'true');
assert.equal(page.inputs[0].focused, true);
assert.equal(page.ids['recipe-prompt'].textContent, descriptionPrompt);
page.ids['copy-starter'].click();
await new Promise((resolve) => setImmediate(resolve));
assert.match(page.copied.at(-1), /Then start any new chat and tell Codex:/);
assert.ok(page.copied.at(-1).endsWith(descriptionPrompt));
const [viewEvent, promptEvent, starterEvent] = page.window.ArchifyStartMetrics.snapshot();
for (const event of [viewEvent, promptEvent, starterEvent]) {
assert.deepEqual(Object.keys(event), ['schemaVersion', 'step', 'source', 'type', 'agent', 'language']);
assert.equal('input' in event, false);
}
assert.deepEqual([viewEvent.step, promptEvent.step, starterEvent.step], ['start_view', 'prompt_copy', 'starter_copy']);
});
test('generated artifacts omit the promotional footer and shortcut manual', () => {
const examples = {
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',
};
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-start-artifacts-'));
try {
for (const [type, input] of Object.entries(examples)) {
const out = path.join(tmp, `${type}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${type}/render-${type}.mjs`),
path.join(skillRoot, 'examples', input),
out,
]);
const html = fs.readFileSync(out, 'utf8');
assert.doesNotMatch(html, /<p class="footer">/, `${type}: footer element`);
assert.doesNotMatch(html, /Built with Archify/, `${type}: product signature`);
assert.doesNotMatch(html, /Create yours/, `${type}: promotional CTA`);
assert.doesNotMatch(html, /Hover to trace/, `${type}: shortcut manual`);
assert.doesNotMatch(html, /source=artifact/, `${type}: removed artifact CTA URL`);
assert.match(html, /id="btn-diagram-guide"/, `${type}: diagram guide remains available`);
const svg = html.match(/<svg[\s\S]*?<\/svg>/)?.[0];
assert.ok(svg, `${type}: SVG missing`);
}
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test('viewer gives wide screens a larger canvas without forcing a subtitle row', () => {
const template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
assert.match(template, /max-width: var\(--archify-reader-width, 1440px\)/);
assert.match(template, /Archify\.readerLayout = \(function \(\)/);
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-title-hierarchy-'));
try {
const input = JSON.parse(fs.readFileSync(
path.join(skillRoot, 'examples', 'web-app.architecture.json'),
'utf8',
));
delete input.meta.subtitle;
const source = path.join(tmp, 'without-subtitle.architecture.json');
const output = path.join(tmp, 'without-subtitle.html');
fs.writeFileSync(source, `${JSON.stringify(input, null, 2)}\n`);
execFileSync(process.execPath, [
path.join(skillRoot, 'renderers', 'architecture', 'render-architecture.mjs'),
source,
output,
]);
assert.doesNotMatch(fs.readFileSync(output, 'utf8'), /class="subtitle"/);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test('artifact-to-install measurement plan separates observable funnel steps from first-diagram success', () => {
const plan = fs.readFileSync(
path.join(repoRoot, 'docs/artifact-install-v2-measurement.md'),
'utf8',
);
for (const required of [
'start_view',
'starter_copy',
'global_install_copy',
'project_install_copy',
'prompt_copy',
'proof_open',
'starter_copy / start_view',
'First-diagram success is not observable from this static page',
'No network request',
'source=artifact',
'source=gallery',
]) {
assert.match(plan, new RegExp(required.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), required);
}
});
@@ -0,0 +1,103 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } 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 template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-story-beat-navigator-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
const result = spawnSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
], { encoding: 'utf8' });
return { result, html: fs.existsSync(output) ? fs.readFileSync(output, 'utf8') : '' };
}
test('all five renderers inherit native inspectable Story Beat controls without changing canonical SVG', () => {
for (const [mode, example] of Object.entries(CASES)) {
const { result, html } = render(mode, example);
assert.equal(result.status, 0, result.stderr);
assert.match(html, /var stop = document\.createElement\('button'\)/);
assert.match(html, /stop\.type = 'button'/);
assert.match(html, /stop\.setAttribute\('aria-label', storyBeatAria\(step, storySteps\.length\)\)/);
assert.match(html, /stop\.setAttribute\('aria-current', 'step'\)/);
assert.doesNotMatch(html, /stop\.setAttribute\('aria-pressed'/);
const generatedSvg = html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
assert.doesNotMatch(generatedSvg, /data-story-(?:active|playing|beat|step|overlay|pulse)/);
}
});
test('adjacent stable IDs classify start, forward, reverse, group, and multiple without inferred cross-links', () => {
assert.match(template, /function storyStep\(view, index, edgeList, byId\)/);
assert.match(template, /from === previousId && to === id/);
assert.match(template, /from === id && to === previousId/);
assert.match(template, /edges\.length === 1 && forward\.length === 1 \? 'forward'/);
assert.match(template, /edges\.length === 1 && reverse\.length === 1 \? 'reverse' : 'multiple'/);
assert.match(template, /index === 0 \? 'start' : \(!edges\.length \? 'group'/);
assert.match(template, /storySteps\.forEach\(function \(step\) \{\s*step\.edges\.forEach/);
assert.doesNotMatch(template, /var storyOrder = \{\}/);
assert.doesNotMatch(template, /Math\.max\(storyOrder/);
});
test('focus pauses without selection while native activation pins one beat and preserves chapter and link ownership', () => {
assert.match(template, /trail\.addEventListener\('focusin'[\s\S]*if \(playing\) pausePlayback\(\)/);
assert.match(template, /trail\.addEventListener\('click'[\s\S]*selectStoryBeat\(Number\(stop\.getAttribute\('data-story-index'\)\)\)/);
assert.match(template, /trail\.addEventListener\('keydown'[\s\S]*event\.key !== 'Enter' && event\.key !== ' '[\s\S]*event\.preventDefault\(\)/);
assert.match(template, /function selectStoryBeat\(index\)/);
assert.match(template, /setStoryBeat\(index, \{ manual: true, center: true, pulse: true, follow: true \}\)/);
assert.match(template, /stop\.setAttribute\('aria-current', 'step'\)/);
assert.match(template, /else stop\.removeAttribute\('aria-current'\)/);
assert.match(template, /trail\.scrollLeft = target/);
const selection = template.match(/function selectStoryBeat\(index\) \{([\s\S]*?)\n function updateUrl/)?.[1] || '';
assert.match(selection, /updateUrl: false/);
assert.doesNotMatch(selection, /history\.|location\.|scrollIntoView/);
assert.match(template, /beat: function \(\)[\s\S]*edgeKeys: step\.edgeKeys\.slice\(\)/);
});
test('one generation-owned scheduler resumes remaining dwell and finite exact-edge signals never loop', () => {
assert.equal((template.match(/storyBeatTimer = setTimeout/g) || []).length, 1);
assert.doesNotMatch(template, /storyBeatTimer = setInterval/);
assert.match(template, /storyPlaybackGeneration \+= 1/);
assert.match(template, /generation !== storyPlaybackGeneration/);
assert.match(template, /preserveElapsed: options\.complete !== true/);
assert.match(template, /storyBeatDwellMs - storyBeatElapsedMs/);
assert.match(template, /afterHandoff\(function \(\)[\s\S]*scheduleStoryPlayback\(\)/);
assert.match(template, /animation: archify-story-flow 0\.78s linear 1 both/);
assert.doesNotMatch(template, /archify-story-flow 0\.78s linear infinite/);
assert.match(template, /svg\[data-preset="blueprint"\]\[data-story-beat\] \[data-story-step\]\[data-story-beat-state="active"\] \{\s*filter: none;\s*animation: none;/);
assert.match(template, /step\.relation !== 'forward' && step\.relation !== 'reverse'/);
assert.match(template, /step\.edges\.length !== 1/);
assert.match(template, /addEventListener\('animationend'[\s\S]*clearStoryPulse/);
});
test('target size, reduced motion, print, embed, and export keep Story Beats viewer-only', () => {
assert.match(template, /\.guided-view-trail \.guided-view-stop \{[\s\S]*min-height: 1\.5rem/);
assert.match(template, /@media \(max-width: 720px\)[\s\S]*\.guided-view-trail \.guided-view-stop \{[\s\S]*min-height: 2rem/);
assert.match(template, /touch-action: pan-x/);
assert.match(template, /document\.documentElement\.getAttribute\('data-embed'\) !== 'true' && typeof MutationObserver !== 'undefined' && typeof Node !== 'undefined' && svg instanceof Node && document\.documentElement instanceof Node/);
assert.match(template, /@media print[\s\S]*\.story-trail-overlay,\s*\.story-carrier-overlay \{ display: none !important; \}/);
assert.match(template, /html\[data-embed="true"\][\s\S]*\.guided-views/);
assert.match(template, /html\[data-motion="still"\] \.story-trail-flow/);
assert.match(template, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.story-trail-flow/);
assert.match(template, /clone\.querySelectorAll\('\[data-story-overlay\], \[data-story-carrier-overlay\]'\)/);
assert.match(template, /clone\.querySelectorAll\('\[data-story-step\], \[data-story-beat-state\], \[data-story-beat-step\]'\)/);
assert.match(template, /canonicalStateClean[\s\S]*data-story-beat-step/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,90 @@
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 template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-story-carrier-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all five renderers inherit one viewer-only Semantic Story Carrier', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /Archify\.flowTokens = \{/, mode);
assert.match(html, /className: 'story-flow-token'/, mode);
assert.match(html, /data-story-carrier-token/, mode);
assert.match(html, /animation: archify-relationship-token-life 0\.78s linear 1 both/, mode);
assert.doesNotMatch(canonicalSvg(html), /story-flow-token|story-carrier-token|semantic-flow-token/, mode);
}
});
test('Story deduplicates SVG path and label fragments by stable authored edge key', () => {
assert.match(template, /function uniqueStoryEdges\(edgeList\)/);
assert.match(template, /var key = storyEdgeKey\(edge\)/);
assert.match(template, /Object\.prototype\.hasOwnProperty\.call\(positions, key\)/);
assert.match(template, /!storyGeometry\(unique\[index\]\)\.length && storyGeometry\(edge\)\.length/);
assert.match(template, /forward = uniqueStoryEdges\(forward\)/);
assert.match(template, /reverse = uniqueStoryEdges\(reverse\)/);
assert.match(template, /edges\.length === 1 && forward\.length === 1 \? 'forward'/);
});
test('Story reuses the exact semantic token vocabulary on its existing finite edge pulse', () => {
assert.match(template, /function createSemanticFlowToken\(edge, shape, options\)/);
assert.match(template, /relationshipTokenGeometry\(shape, relationshipTokenKind\(edge\), key, options\)/);
assert.match(template, /Archify\.flowTokens\.create\(edge, shapes\[0\], \{/);
assert.match(template, /className: 'story-flow-token'/);
assert.match(template, /duration: '0\.78s'/);
assert.match(template, /carrier\.setAttribute\('data-story-beat-step', String\(step\.index\)\)/);
assert.match(template, /carrierOverlay\.setAttribute\('data-story-carrier-overlay', ''\)/);
assert.match(template, /carrierWrapper\.appendChild\(carrier\)/);
assert.match(template, /svg\.insertBefore\(carrierOverlay, firstNode\)/);
assert.match(template, /semantic-flow-token-halo/);
assert.doesNotMatch(template, /story-flow-token[^}]+infinite/);
});
test('only explicit play=1 embeds may show the finite carrier', () => {
assert.match(template, /data-embed'\) === 'true' &&\s*document\.documentElement\.getAttribute\('data-share-playback'\) !== 'true'/);
assert.match(template, /autoplayPending = sharePlaybackRequested\(\)/);
assert.match(template, /document\.documentElement\.setAttribute\('data-share-playback', 'true'\)/);
assert.match(template, /html\[data-motion="still"\] \.story-carrier-overlay/);
assert.match(template, /html\[data-document-hidden="true"\] \.story-carrier-overlay/);
assert.match(template, /@media \(prefers-reduced-motion: reduce\)[\s\S]+\.story-carrier-overlay \{ display: none !important; \}/);
});
test('Story Carrier cleanup and export remain owned by Story Trail', () => {
assert.match(template, /svg\.querySelectorAll\('\[data-story-carrier-overlay\]'\)/);
assert.match(template, /overlay\.remove\(\)/);
assert.match(template, /var pulseGeneration = storyPulseGeneration/);
assert.match(template, /if \(pulseGeneration === storyPulseGeneration\) clearStoryPulse\(\)/);
assert.equal((template.match(/storyPulseOwnerToken = Archify\.motionGovernor\.claim\('story'/g) || []).length, 1);
assert.match(template, /clone\.querySelectorAll\('\[data-story-overlay\], \[data-story-carrier-overlay\]'\)/);
assert.match(template, /@media print \{[\s\S]+\.story-trail-overlay,[\s\S]+\.story-carrier-overlay \{ display: none !important; \}/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,91 @@
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 template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-story-director-strip-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all five renderers inherit one viewer-only Story Director Strip', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /id="guided-story-caption" hidden aria-live="polite" aria-atomic="true"/, mode);
assert.match(html, /function renderStoryCaption\(step, total, nextStep\)/, mode);
assert.match(html, /storyCaptionRoute\.textContent = storyCaptionRouteCopy\(step\)/, mode);
assert.match(html, /storyCaptionDetail\.textContent = storyCaptionDetailCopy\(step\)/, mode);
assert.doesNotMatch(canonicalSvg(html), /guided-story-caption|data-story-caption/, mode);
}
});
test('captions derive only authored edge labels and existing node facts', () => {
assert.match(template, /edgeLabels: edges\.map\(function \(edge\) \{ return edge\.getAttribute\('data-edge-label'\) \|\| ''; \}\)/);
assert.match(template, /responsibility: node \? \(node\.getAttribute\('data-node-sublabel'\) \|\| ''\) : ''/);
assert.match(template, /context: node \? \(node\.getAttribute\('data-node-context'\) \|\| ''\) : ''/);
assert.match(template, /step\.edgeLabels\.slice\(0, 3\)\.join\(' \+ '\)/);
assert.match(template, /viewerText\('viewer\.guided\.caption\.grouped'\)/);
assert.match(template, /if \(!facts\.length\) facts\.push\(viewerText\('viewer\.guided\.caption\.starting'\)\)/);
assert.match(template, /viewerText\('viewer\.guided\.caption\.direction'/);
assert.doesNotMatch(template, /inferred relationship|likely transition|calls service/);
});
test('route copy preserves start, forward, reverse, multiple, and grouped semantics', () => {
assert.match(template, /viewerText\('viewer\.guided\.beat\.start'/);
assert.match(template, /viewerText\('viewer\.guided\.beat\.forward'/);
assert.match(template, /viewerText\('viewer\.guided\.beat\.reverse'/);
assert.match(template, /viewerText\('viewer\.guided\.beat\.multiple'/);
assert.match(template, /viewerText\('viewer\.guided\.beat\.group'/);
});
test('playback announcements and motion remain reader-controlled', () => {
assert.match(template, /storyCaption\.setAttribute\('aria-live', playing \? 'off' : 'polite'\)/);
assert.match(template, /html\[data-motion="still"\] \.guided-story-caption/);
assert.match(template, /@media \(prefers-reduced-motion: reduce\) \{\s*\.guided-story-caption \{ animation: none !important; \}/);
assert.match(template, /animation: archify-story-caption-in 140ms/);
});
test('Presentation playback removes secondary chrome without hiding Pause or navigation', () => {
assert.match(template, /\.guided-views\[data-story-beat\] \.guided-view-copy > #guided-view-label/);
assert.match(template, /@media \(min-width: 721px\)[\s\S]*\.guided-views\[data-playing="true"\] \.guided-view-index/);
assert.match(template, /\.guided-views\[data-playing="true"\] \.guided-view-beat-link/);
assert.match(template, /\.guided-views\[data-playing="true"\] \.guided-view-all/);
const presentationRule = template.match(/@media \(min-width: 721px\) \{([\s\S]*?)\n \}/)?.[1] || '';
assert.doesNotMatch(presentationRule, /guided-view-play/);
assert.doesNotMatch(presentationRule, /guided-view-prev|guided-view-next/);
assert.match(template, /@media \(max-width: 720px\)[\s\S]*\.guided-views > #guided-view-prev,[\s\S]*height: 2\.75rem/);
assert.match(template, /@media \(max-width: 720px\)[\s\S]*\.guided-view-play,[\s\S]*\.guided-view-beat-link \{ min-height: 2\.75rem; \}/);
});
test('embed, print, and canonical export boundaries stay clean', () => {
assert.match(template, /html\[data-embed="true"\] \.guided-views \{ display: none !important; \}/);
assert.match(template, /@media print[\s\S]*\.guided-views/);
assert.doesNotMatch(canonicalSvg(render('workflow', CASES.workflow)), /Story Director|guided-story-caption|data-story-caption/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,103 @@
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 template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-story-follow-camera-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all five renderers inherit one viewer-only Story Follow Camera', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /function followStoryStep\(step, options\)/, mode);
assert.match(html, /panel\.setAttribute\('data-story-follow', 'moving'\)/, mode);
assert.match(html, /panel\.setAttribute\('data-story-follow-node', step\.nodeId\)/, mode);
assert.doesNotMatch(canonicalSvg(html), /data-story-follow/, mode);
}
});
test('Story Follow frames the exact previous, current, and next authored stops through the shared camera', () => {
assert.match(template, /function storyFrameIds\(step\)/);
assert.match(template, /step\.index > 0 && storySteps\[step\.index - 1\]/);
assert.match(template, /ids\.push\(step\.nodeId\)/);
assert.match(template, /step\.index \+ 1 < storySteps\.length/);
assert.match(template, /var ids = storyFrameIds\(step\)/);
assert.match(template, /Archify\.view\.reveal\(ids, \{/);
assert.match(template, /reason: options\.manual === true \? 'story-beat' : 'story-follow'/);
assert.match(template, /padding: 64/);
assert.match(template, /maxScale: 1\.65/);
assert.match(template, /duration: STORY_FOLLOW_DURATION_MS/);
assert.match(template, /storyFollowGeneration/);
assert.match(template, /generation !== storyFollowGeneration \|\| storyBeatIndex !== step\.index/);
});
test('playback and deliberate beat activation follow while stable moment restoration stays calm', () => {
assert.match(template, /setStoryBeat\(0, \{ pulse: true, follow: true \}\)/);
assert.match(template, /setStoryBeat\(storyBeatIndex \+ 1, \{ pulse: true, follow: true \}\)/);
assert.match(template, /if \(storyBeatIndex >= 0\) followStoryStep\(storySteps\[storyBeatIndex\]\)/);
assert.match(template, /setStoryBeat\(index, \{ manual: true, center: true, pulse: true, follow: true \}\)/);
assert.match(template, /follow: options\.follow === true/);
assert.match(template, /selectStoryBeatById\(requestedBeat, \{ linked: true, follow: true, followInstant: true \}\)/);
assert.match(template, /if \(embed && !explicitEmbedPlayback && options\.linked !== true\) return false/);
assert.match(template, /var deferredGeneration = \+\+storyFollowGeneration/);
assert.match(template, /requestAnimationFrame\(function \(\) \{[\s\S]*?storyBeatIndex !== deferredIndex[\s\S]*?followStoryStep\(step, options\)/);
});
test('adaptive dwell, Still, reduced motion, hidden pages, and print keep camera motion bounded', () => {
assert.match(template, /var STORY_FOLLOW_MIN_DWELL_MS = 1100/);
assert.match(template, /var STORY_FOLLOW_DURATION_MS = 320/);
assert.match(template, /Math\.max\(STORY_FOLLOW_MIN_DWELL_MS, VIEW_INTERVAL_MS \/ Math\.max\(1, total\)\)/);
assert.match(template, /storyBeatDwellMs = storyBeatDwell\(total\)/);
assert.match(template, /if \(!step \|\| document\.hidden/);
assert.match(template, /window\.matchMedia\('print'\)\.matches/);
assert.match(template, /instant: options\.instant === true \|\| reducedMotion\(\) \|\| document\.documentElement\.getAttribute\('data-motion'\) !== 'live'/);
assert.match(template, /function storyAutomaticPlaybackAllowed\(\)/);
assert.match(template, /Archify\.motionGovernor && Archify\.motionGovernor\.capable\) return !Archify\.motionGovernor\.isPaused\(\)/);
assert.match(template, /play\.disabled = !playing && !automaticPlaybackAllowed/);
assert.match(template, /'viewer\.guided\.motionUnavailable'/);
assert.match(template, /function startPlayback\(\) \{[\s\S]*?if \(!storyAutomaticPlaybackAllowed\(\)\)/);
assert.match(template, /if \(shouldPlay && svg\.getAttribute\('data-story-playing'\) !== 'true'\)/);
assert.match(template, /else if \(!shouldPlay && svg\.hasAttribute\('data-story-playing'\)\)/);
assert.doesNotMatch(template, /storyFollowTimer = setInterval/);
});
test('pause, settle, overview, and manual camera takeover cancel Story Follow state', () => {
assert.match(template, /function clearStoryFollow\(\)/);
assert.match(template, /svg\.removeAttribute\('data-story-follow'\)/);
assert.match(template, /panel\.removeAttribute\('data-story-follow'\)/);
assert.match(template, /function pausePlayback\(options\)[\s\S]*?clearStoryFollow\(\)/);
assert.match(template, /function settleStoryBeats\(\)[\s\S]*?clearStoryFollow\(\)/);
assert.match(template, /function clearStoryTrail\(\)[\s\S]*?clearStoryFollow\(\)/);
assert.match(template, /function interruptCamera\(reason\)[\s\S]*?Archify\.guidedViews\.pause\(\)/);
assert.match(template, /clone\.removeAttribute\('data-story-follow'\)/);
assert.match(template, /!clone\.hasAttribute\('data-story-follow'\)/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,89 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } 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 template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-story-horizon-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
const result = spawnSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
], { encoding: 'utf8' });
return { result, html: fs.existsSync(output) ? fs.readFileSync(output, 'utf8') : '' };
}
test('all five renderers inherit one viewer-only Story Horizon', () => {
for (const [mode, example] of Object.entries(CASES)) {
const { result, html } = render(mode, example);
assert.equal(result.status, 0, result.stderr);
assert.match(html, /data-story-beat-state="next"/, mode);
assert.match(html, /step === storyBeatIndex \+ 1/, mode);
assert.match(html, /svg\.setAttribute\('data-story-next', nextStep\.nodeId\)/, mode);
assert.match(html, /panel\.setAttribute\('data-story-next', nextStep\.nodeId\)/, mode);
assert.match(html, /id="guided-story-caption-next" hidden aria-hidden="true"/, mode);
const generatedSvg = html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
assert.doesNotMatch(generatedSvg, /data-story-next|data-story-beat-state="next"/, mode);
}
});
test('the temporal hierarchy has one bounded next state and a clean final beat', () => {
assert.match(template, /if \(step < storyBeatIndex\) return 'past'/);
assert.match(template, /if \(step === storyBeatIndex\) return 'active'/);
assert.match(template, /if \(step === storyBeatIndex \+ 1\) return 'next'/);
assert.match(template, /return 'pending'/);
assert.match(template, /storyBeatIndex \+ 1 < storySteps\.length \? storySteps\[storyBeatIndex \+ 1\] : null/);
assert.match(template, /else \{\s*svg\.removeAttribute\('data-story-next'\);\s*panel\.removeAttribute\('data-story-next'\)/);
assert.match(template, /storyCaptionNext\.hidden = !nextStep/);
});
test('next edges reuse exact authored step membership without synthesizing topology', () => {
assert.match(template, /storySteps\.forEach\(function \(step\) \{\s*step\.edges\.forEach/);
assert.match(template, /edge\.setAttribute\('data-story-beat-step', String\(edgeBeat\)\)/);
assert.match(template, /edge\.setAttribute\('data-story-beat-state', storyBeatState\(step\)\)/);
assert.match(template, /index === 0 \? 'start' : \(!edges\.length \? 'group'/);
assert.match(template, /edges\.length === 1 && forward\.length === 1 \? 'forward'/);
assert.match(template, /edges\.length === 1 && reverse\.length === 1 \? 'reverse' : 'multiple'/);
assert.doesNotMatch(template, /createElementNS\([^\n]+story-horizon|data-story-horizon-edge/);
});
test('next remains static, subordinate, preset-safe, and mobile-height neutral', () => {
assert.match(template, /data-story-step\]\[data-story-beat-state="next"\] \{\s*opacity: 0\.5;\s*filter: saturate\(0\.66\)/);
assert.match(template, /data-story-beat-state="past"\] \{\s*opacity: 0\.72/);
assert.match(template, /data-edge-from\]\[data-story-beat-step\]\[data-story-beat-state="next"\] \{ opacity: 0\.34; \}/);
assert.match(template, /guided-view-stop\[data-story-beat-state="next"\][\s\S]*border-style: dashed/);
assert.match(template, /@media \(max-width: 720px\)[\s\S]*\.guided-story-caption-next \{ display: none; \}/);
const nextRules = template.match(/[^\n{]*data-story-beat-state="next"[^\n{]*\{[^}]*\}/g)?.join('\n') || '';
assert.doesNotMatch(nextRules, /animation:|drop-shadow|stroke-dasharray/);
});
test('Still, accessibility, teardown, and export preserve the product boundary', () => {
assert.match(template, /id="guided-story-caption-next" hidden aria-hidden="true"/);
assert.match(template, /storyCaption\.setAttribute\('aria-live', playing \? 'off' : 'polite'\)/);
assert.doesNotMatch(template, /storyCaptionNext\.setAttribute\('aria-live'/);
assert.match(template, /html\[data-motion="still"\] \[data-story-step\]/);
assert.match(template, /html\[data-motion="still"\] svg \[data-node-id\][\s\S]*transition: none !important/);
assert.match(template, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.story-trail-flow/);
assert.ok((template.match(/svg\.removeAttribute\('data-story-next'\)/g) || []).length >= 3);
assert.ok((template.match(/panel\.removeAttribute\('data-story-next'\)/g) || []).length >= 2);
assert.match(template, /clone\.removeAttribute\('data-story-next'\)/);
assert.match(template, /canonicalStateClean[\s\S]*!clone\.hasAttribute\('data-story-next'\)/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,93 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } 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 template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-story-moment-link-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
const result = spawnSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
], { encoding: 'utf8' });
return { result, html: fs.existsSync(output) ? fs.readFileSync(output, 'utf8') : '' };
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all five renderers inherit one viewer-only Story Moment Link control', () => {
for (const [mode, example] of Object.entries(CASES)) {
const { result, html } = render(mode, example);
assert.equal(result.status, 0, result.stderr);
assert.match(html, /id="guided-view-beat-link"[^>]+aria-label="Select a Story Beat to copy its exact link"[^>]+disabled/);
assert.match(html, /id="guided-view-beat-link-label">Copy moment<\/span>/);
assert.doesNotMatch(canonicalSvg(html), /data-story-moment|guided-view-beat-link|#view=/);
}
});
test('moment links use exact stable view and node ids without mutating manual selection URLs', () => {
assert.match(template, /function storyMomentLink\(\)/);
assert.match(template, /url\.searchParams\.delete\('play'\)/);
assert.match(template, /url\.hash = 'view=' \+ encodeURIComponent\(view\.id\) \+ '&beat=' \+ encodeURIComponent\(step\.nodeId\)/);
assert.match(template, /function selectStoryBeatById\(id, options\)/);
assert.match(template, /storySteps\.findIndex\(function \(step\) \{ return step\.nodeId === id; \}\)/);
assert.match(template, /var requestedBeat = params\.get\('beat'\)/);
assert.match(template, /var restoreGeneration = \+\+momentRestoreGeneration/);
assert.match(template, /afterHandoff\(function \(\) \{[\s\S]*restoreGeneration !== momentRestoreGeneration[\s\S]*selectStoryBeatById\(requestedBeat, \{ linked: true, follow: true, followInstant: true \}\)/);
const manualSelection = template.match(/function selectStoryBeat\(index\) \{([\s\S]*?)\n function updateUrl/)?.[1] || '';
assert.doesNotMatch(manualSelection, /history\.|location\.|updateUrl\(/);
});
test('invalid or cross-chapter beat ids fail closed while the public receipt stays read-only', () => {
assert.match(template, /if \(index < 0\) return false/);
assert.doesNotMatch(template, /Number\(params\.get\('beat'\)\)/);
assert.match(template, /setStoryBeat\(index, \{[\s\S]*?manual: false,[\s\S]*?center: true,[\s\S]*?pulse: false,[\s\S]*?follow: options\.follow === true,[\s\S]*?linked: options\.linked === true,[\s\S]*?followInstant: options\.followInstant === true/);
assert.match(template, /beatLink: storyMomentLink/);
assert.match(template, /copyBeatLink: copyStoryMomentLink/);
assert.match(template, /beat: function \(\)[\s\S]*nodeId: step\.nodeId/);
});
test('copy feedback, one-shot playback, and reduced motion preserve the requested moment', () => {
assert.match(template, /navigator\.clipboard && typeof navigator\.clipboard\.writeText === 'function'/);
assert.match(template, /navigator\.clipboard\.writeText\(value\)/);
assert.match(template, /document\.execCommand\('copy'\)/);
assert.match(template, /beatLinkLabel\.textContent = viewerText\(copied \? 'viewer\.guided\.copied' : 'viewer\.guided\.copyFailed'\)/);
assert.match(template, /viewerText\(copied \? 'viewer\.guided\.momentCopied' : 'viewer\.guided\.momentCopyFailed'\)/);
assert.match(template, /function hashBeatMatchesCurrent\(\)/);
assert.match(template, /if \(!storyAutomaticPlaybackAllowed\(\)\)[\s\S]*hashBeatMatchesCurrent\(\)[\s\S]*setAutoplayState\('reduced-motion'\)/);
assert.match(template, /storyPlaybackScope = 'chapter'/);
assert.match(template, /if \(storyBeatIndex < 0\)[\s\S]*setStoryBeat\(0, \{ pulse: true, follow: true \}\)/);
});
test('the control keeps stable desktop/mobile geometry and existing viewer boundaries', () => {
assert.match(template, /\.guided-view-beat-link \{[\s\S]*min-height: 1\.5rem/);
assert.match(template, /@media \(max-width: 720px\)[\s\S]*\.guided-view-actions \{[\s\S]*grid-template-columns: repeat\(3, minmax\(0, 1fr\)\)/);
assert.match(template, /@media \(max-width: 720px\)[\s\S]*\.guided-view-beat-link \{[\s\S]*min-height: 2\.75rem/);
assert.match(template, /html\[data-embed="true"\][\s\S]*\.guided-views \{ display: none !important; \}/);
assert.match(template, /html\[data-embed="true"\]\[data-share-moment="true"\] \.share-chapter-cue:not\(\[hidden\]\)/);
assert.match(template, /pinnedMode = !shareMode && embedMode && hashBeatMatchesCurrent\(\)/);
assert.match(template, /pinned: viewerText\('viewer\.guided\.state\.pinned'\)/);
assert.match(template, /@media print[\s\S]*\.guided-views/);
assert.match(template, /syncStoryControlsDisabled\(\)[\s\S]*beatLink\.disabled =/);
assert.match(template, /clone\.querySelectorAll\('\[data-story-overlay\], \[data-story-carrier-overlay\]'\)/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,84 @@
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 template = fs.readFileSync(path.join(skillRoot, 'assets/template.html'), 'utf8');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-story-shelf-'));
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) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', CASES[mode]),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function canonicalSvg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all five renderers inherit one compact cold-open Story Shelf', () => {
for (const mode of Object.keys(CASES)) {
const html = render(mode);
assert.match(html, /\.guided-views\[data-active-view="all"\]\s*\{/, mode);
assert.match(html, /panel\.setAttribute\('data-active-view', view \? view\.id : 'all'\)/, mode);
assert.doesNotMatch(canonicalSvg(html), /Story Shelf|data-active-view|guided-view-index/, mode);
}
});
test('cold shelf keeps chapter identity and Play while removing only unavailable duplicate controls', () => {
assert.match(template, /\.guided-views\[data-active-view="all"\] > #guided-view-prev,[\s\S]*#guided-view-next,[\s\S]*\.guided-view-beat-link,[\s\S]*\.guided-view-all\s*\{\s*display: none;\s*\}/);
assert.doesNotMatch(template, /\.guided-views\[data-active-view="all"\][^{]*(?:\.guided-view-play|\.guided-view-index)[^{]*\{\s*display:\s*none/);
assert.match(template, /<button class="guided-view-play"/);
assert.match(template, /<nav class="guided-view-index"/);
});
test('desktop shelf follows DOM order and returns vertical space to the diagram', () => {
const rule = template.match(/\.guided-views\[data-active-view="all"\]\s*\{([^}]*)\}/)?.[1] || '';
assert.match(rule, /grid-template-columns:\s*minmax\(11rem,\s*\.72fr\)\s+auto\s+minmax\(0,\s*2fr\)/);
assert.match(template, /\.guided-views\[data-active-view="all"\] \.guided-view-copy\s*\{[^}]*grid-column:\s*1/);
assert.match(template, /\.guided-views\[data-active-view="all"\] \.guided-view-actions\s*\{[^}]*grid-column:\s*2/);
assert.match(template, /\.guided-views\[data-active-view="all"\] \.guided-view-index\s*\{[^}]*grid-column:\s*3/);
assert.match(template, /\.guided-views\[data-active-view="all"\] \.guided-view-copy > #guided-view-note\s*\{\s*display:\s*none/);
});
test('mobile shelf preserves 44px controls, horizontal chapters, and honest expansion', () => {
assert.match(template, /@media \(max-width: 720px\)[\s\S]*\.guided-views\[data-active-view="all"\][\s\S]*grid-template-columns:\s*minmax\(0,\s*1fr\)\s+auto/);
assert.match(template, /\.guided-views\[data-active-view="all"\] \.guided-view-index\s*\{[^}]*grid-column:\s*1 \/ -1;[^}]*grid-row:\s*2/);
assert.match(template, /\.guided-views\[data-active-view="all"\] \.guided-view-play\s*\{[^}]*min-height:\s*2\.75rem/);
assert.match(template, /\.guided-view-chapters\s*\{[^}]*overflow-x:\s*auto/);
assert.match(template, /\.guided-view-chapter\s*\{[^}]*min-height:\s*2\.75rem/);
});
test('active stories expand through existing state without storage or a second interaction owner', () => {
assert.match(template, /panel\.setAttribute\('data-active-view', 'all'\);\s*panel\.hidden = false/);
assert.match(template, /data-active-view', view \? view\.id : 'all'/);
assert.match(template, /if \(activeIndex < 0\) activate\(0, \{ playback: true \}\)/);
assert.match(template, /showAll\([\s\S]*activeIndex = -1;[\s\S]*render\(\)/);
assert.doesNotMatch(template, /storyShelf(?:Open|Expanded|Storage)|archify-story-shelf|localStorage[^\n]*shelf/i);
});
test('Story Shelf remains viewer-only, embed-safe, print-safe, and motion-neutral', () => {
assert.match(template, /html\[data-embed="true"\] \.guided-views \{ display: none !important; \}/);
assert.match(template, /\.toolbar, \.diagram-nav, \.focus-chip, \.guided-views, \.archify-toast, \.no-print \{ display: none !important; \}/);
assert.doesNotMatch(canonicalSvg(render('workflow')), /Story Shelf|guided-view|data-active-view/);
assert.doesNotMatch(template, /@keyframes\s+archify-story-shelf|animation:[^;]*story-shelf/i);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,81 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } 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-story-trail-'));
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, example) {
const output = path.join(tmp, `${mode}.html`);
const result = spawnSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
], { encoding: 'utf8' });
return { result, html: fs.existsSync(output) ? fs.readFileSync(output, 'utf8') : '' };
}
for (const [mode, example] of Object.entries(CASES)) {
test(`${mode}: guided paths expose a viewer-only Story Trail`, () => {
const { result, html } = render(mode, example);
assert.equal(result.status, 0, result.stderr);
assert.match(html, /id="guided-view-trail" hidden role="group" aria-label="Story trail"/);
assert.match(html, /function renderStoryTrail\(view\)/);
assert.match(html, /document\.createElement\('button'\)/);
assert.match(html, /stop\.type = 'button'/);
assert.match(html, /data-story-node/);
assert.match(html, /data-story-link/);
assert.match(html, /edges\.length === 1 && forward\.length === 1 \? 'forward'/);
assert.match(html, /data-story-overlay/);
assert.match(html, /data-story-playing/);
assert.match(html, /data-story-beat/);
assert.match(html, /data-story-beat-state/);
assert.match(html, /data-story-beat-step/);
assert.match(html, /storySteps\.forEach\(function \(step\)/);
assert.match(html, /step\.edges\.forEach\(function \(edge\)/);
assert.match(html, /edge\.setAttribute\('data-story-beat-step', String\(edgeBeat\)\)/);
assert.match(html, /svg\[data-story-beat\] \[data-edge-from\]\[data-story-beat-step\]/);
assert.match(html, /story-trail-flow/);
assert.match(html, /function scheduleStoryPlayback\(\)/);
assert.match(html, /storyBeatTimer = setTimeout/);
assert.match(html, /storyBeatDwellMs = storyBeatDwell\(total\)/);
assert.match(html, /Math\.max\(STORY_FOLLOW_MIN_DWELL_MS, VIEW_INTERVAL_MS \/ Math\.max\(1, total\)\)/);
assert.match(html, /function storyStep\(view, index, edgeList, byId\)/);
assert.match(html, /prefers-reduced-motion: reduce/);
assert.match(html, /from === previousId && to === id/);
assert.match(html, /from === id && to === previousId/);
assert.match(html, /firstEdge\.parentNode\.insertBefore\(overlay, firstEdge\)/);
assert.doesNotMatch(html, /content: '\\2192';\s*font-size: 0\.65rem/);
const generatedSvg = html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
assert.doesNotMatch(generatedSvg, /data-story-(?:active|playing|beat|step|overlay|carrier)/);
});
}
test('Story Trail state is removed from every export clone', () => {
const template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
assert.match(template, /clone\.removeAttribute\('data-story-active'\)/);
assert.match(template, /clone\.removeAttribute\('data-story-playing'\)/);
assert.match(template, /clone\.removeAttribute\('data-story-beat'\)/);
assert.match(template, /clone\.querySelectorAll\('\[data-story-overlay\], \[data-story-carrier-overlay\]'\)/);
assert.match(template, /clone\.querySelectorAll\('\[data-story-step\], \[data-story-beat-state\], \[data-story-beat-step\]'\)/);
assert.match(template, /el\.removeAttribute\('data-story-beat-state'\)/);
assert.match(template, /el\.removeAttribute\('data-story-beat-step'\)/);
assert.match(template, /el\.style\.removeProperty\('--story-step'\)/);
assert.match(template, /canonicalStateClean[\s\S]*data-story-beat-state/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
@@ -0,0 +1,57 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const template = fs.readFileSync(path.resolve(__dirname, '../assets/template.html'), 'utf8');
test('toolbar keeps four independent controls with explicit open states', () => {
assert.match(template, /\.toolbar \{[\s\S]*?gap: 0\.5rem;[\s\S]*?padding: 0;[\s\S]*?background: transparent;[\s\S]*?box-shadow: none;/);
assert.match(template, /\.toolbar button \{[\s\S]*?background: var\(--toolbar-bg\);[\s\S]*?border: 1px solid var\(--toolbar-border\);/);
assert.match(template, /button\[aria-expanded="true"\]/);
assert.doesNotMatch(template, /\.preset-wrap::before,[\s\S]*?\.export-wrap::before/);
assert.match(template, /<span id="theme-icon" class="toolbar-icon"/);
assert.match(template, /<span id="present-icon" class="toolbar-icon"/);
assert.doesNotMatch(template.match(/<div class="toolbar"[\s\S]*?<div class="container">/)?.[0] || '', /<svg\b/);
assert.doesNotMatch(template, /icon\.textContent =/);
});
test('export menu has grouped, single-column rows and a zoom-safe width', () => {
const sectionCss = template.match(/\.export-menu-section \{[\s\S]*?\}/)?.[0] || '';
assert.match(template, /class="export-menu-section" role="group" aria-label="\{\{i18n:viewer\.export\.share\}\}"/);
assert.match(template, /class="export-menu-section" role="group" aria-label="\{\{i18n:viewer\.export\.raster\}\}"/);
assert.match(template, /class="export-menu-section" role="group" aria-label="\{\{i18n:viewer\.export\.vectorMotion\}\}"/);
assert.match(template, /class="export-menu-header" role="presentation"/);
assert.match(template, /\.toolbar \.export-menu \{[\s\S]*?width: 19rem;[\s\S]*?max-width: calc\(100vw - 2rem\);/);
assert.match(template, /\.export-menu-section \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\);/);
assert.doesNotMatch(sectionCss, /repeat\(2/);
assert.match(template, /\.toolbar \.export-menu button \{[\s\S]*?grid-template-columns: 1\.15rem minmax\(0, 1fr\);[\s\S]*?white-space: nowrap;/);
assert.match(template, /\.export-item-copy strong,[\s\S]*?\.export-item-copy small \{ display: block; \}/);
});
test('mobile menus share one viewport-safe placement and disabled exports remain explicit', () => {
assert.match(template, /@media \(max-width: 720px\)[\s\S]*?\.toolbar \.preset-menu,[\s\S]*?\.toolbar \.export-menu \{[\s\S]*?position: fixed;/);
assert.match(template, /\.toolbar \.export-menu button:disabled \{[\s\S]*?cursor: not-allowed;/);
assert.doesNotMatch(template, /it\.style\.opacity = '0\.5'/);
});
test('diagram view dock stays compact on desktop and touch-safe on narrow screens', () => {
assert.match(template, /\.diagram-nav \{[\s\S]*?padding: 0\.15rem;[\s\S]*?border-radius: 0\.58rem;/);
assert.match(template, /\.diagram-nav button \{[\s\S]*?min-width: 2rem;[\s\S]*?height: 2rem;/);
assert.match(template, /@media \(max-width: 720px\)[\s\S]*?\.diagram-nav button \{[\s\S]*?min-width: 2\.75rem;[\s\S]*?height: 2\.75rem;/);
assert.match(template, /class="diagram-nav-icon find"/);
assert.match(template, /class="diagram-nav-icon guide"/);
assert.match(template, /class="diagram-nav-icon minus"/);
assert.match(template, /class="diagram-nav-icon plus"/);
});
test('diagram view reset separates semantic detail from zoom percentage', () => {
assert.match(template, /data-view="reset"[\s\S]*?data-view-detail hidden>\{\{i18n:viewer\.nav\.read\}\}<[\s\S]*?data-view-percent>100%</);
assert.match(template, /var resolvedLevel = semantic \? viewerText\('viewer\.nav\.level\.auto'\) : levelLabel;/);
assert.match(template, /var showDetailLevel = semantic \|\| detail !== 'read';/);
assert.match(template, /resetDetailLabel\.hidden = !showDetailLevel/);
assert.match(template, /resetPercentLabel\.textContent = percent/);
assert.match(template, /resetBtn\.toggleAttribute\('data-detail-visible', showDetailLevel\)/);
});

Some files were not shown because too many files have changed in this diff Show More