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, pathToFileURL } from 'node:url';
import { createHash } from 'node:crypto';
import {
ArchitectureDeltaError,
architectureDeltaChangeRows,
canonicalArchitectureJson,
compareArchitecture,
renderArchitectureDeltaHtml,
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' });
function git(repo, ...args) {
return execFileSync('git', ['-C', repo, ...args], {
encoding: 'utf8',
env: {
...process.env,
GIT_AUTHOR_DATE: '2026-01-01T00:00:00Z',
GIT_COMMITTER_DATE: '2026-01-01T00:00:00Z',
},
}).trim();
}
function provenanceFixture() {
const root = fs.mkdtempSync(path.join(tmp, 'provenance-repo-'));
const sourceDirectory = path.join(root, 'src');
fs.mkdirSync(sourceDirectory);
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/provenance-repo.git');
fs.writeFileSync(path.join(sourceDirectory, 'service.js'), 'export const version = 1;\n');
git(root, 'add', 'src/service.js');
git(root, 'commit', '-m', 'base');
const baseRevision = git(root, 'rev-parse', 'HEAD');
fs.writeFileSync(path.join(sourceDirectory, 'service.js'), 'export const version = 2;\n');
git(root, 'add', 'src/service.js');
git(root, 'commit', '-m', 'head');
const headRevision = git(root, 'rev-parse', 'HEAD');
const diagram = {
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Provenance-only delta', output: 'provenance-only-delta.html' },
components: [{
id: 'service',
type: 'backend',
label: 'Service',
pos: [100, 100],
size: [160, 80],
sources: [{ path: 'src/service.js', line: 1 }],
}],
boundaries: [],
connections: [],
cards: [],
};
const base = structuredClone(diagram);
base.meta.repository = { url: 'https://github.com/example/provenance-repo', revision: baseRevision };
const head = structuredClone(diagram);
head.meta.repository = { url: 'https://github.com/example/provenance-repo', revision: headRevision };
const basePath = path.join(root, 'base.architecture.json');
const headPath = path.join(root, 'head.architecture.json');
fs.writeFileSync(basePath, JSON.stringify(base));
fs.writeFileSync(headPath, JSON.stringify(head));
return { root, basePath, headPath, baseRevision, headRevision };
}
const renderDelta = (receipt) => renderArchitectureDeltaHtml({
receipt,
baseSvg: '',
deltaSvg: '',
headSvg: '',
baseHtml: '
Before',
headHtml: 'After',
artifactCss: '',
});
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('compare reports brand-only changes in the receipt and exact review target', () => {
const base = {
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Cache', output: 'cache.html' },
components: [{ id: 'cache', type: 'database', label: 'Cache', pos: [100, 100], size: [160, 80] }],
};
const basePath = path.join(tmp, 'brand-base.json');
const headPath = path.join(tmp, 'brand-head.json');
const output = path.join(tmp, 'brand-delta.html');
for (const [before, after] of [[undefined, 'redis'], ['redis', 'postgresql'], ['redis', undefined]]) {
const head = structuredClone(base);
base.components[0].brand = before;
head.components[0].brand = after;
fs.writeFileSync(basePath, JSON.stringify(base));
fs.writeFileSync(headPath, JSON.stringify(head));
const result = run(['compare', 'architecture', basePath, headPath, output, '--json']);
assert.equal(result.status, 0, result.stderr);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.summary.components.changed, 1);
assert.deepEqual(receipt.changes.components[0].classifications, ['semantic']);
assert.deepEqual(receipt.changes.components[0].changedFields, ['/brand']);
const html = fs.readFileSync(output, 'utf8');
assert.match(html, /data-change-key="component:cache"/);
assert.deepEqual(validateArchitectureDeltaHtml(html, receipt), { ok: true, checksPassed: 10, checkCount: 10 });
}
});
test('compare reports icon-only changes in the receipt and exact review target', () => {
const base = {
schema_version: 1,
diagram_type: 'architecture',
meta: { title: 'Cache', output: 'cache.html' },
components: [{ id: 'cache', type: 'database', label: 'Cache', pos: [100, 100], size: [160, 80] }],
};
const basePath = path.join(tmp, 'icon-base.json');
const headPath = path.join(tmp, 'icon-head.json');
const output = path.join(tmp, 'icon-delta.html');
for (const [before, after] of [[undefined, 'clock'], ['clock', 'moon'], ['moon', 'none'], ['none', undefined]]) {
const head = structuredClone(base);
base.components[0].icon = before;
head.components[0].icon = after;
fs.writeFileSync(basePath, JSON.stringify(base));
fs.writeFileSync(headPath, JSON.stringify(head));
const result = run(['compare', 'architecture', basePath, headPath, output, '--json']);
assert.equal(result.status, 0, result.stderr);
const receipt = JSON.parse(result.stdout);
assert.equal(receipt.summary.components.changed, 1);
assert.deepEqual(receipt.changes.components[0].classifications, ['semantic']);
assert.deepEqual(receipt.changes.components[0].changedFields, ['/icon']);
const html = fs.readFileSync(output, 'utf8');
assert.match(html, /data-change-key="component:cache"/);
assert.deepEqual(validateArchitectureDeltaHtml(html, receipt), { ok: true, checksPassed: 10, checkCount: 10 });
}
});
test('compare reports locale-only changes as presentation changes', () => {
const head = read(baseFixture);
head.meta.locale = 'zh-CN';
const headPath = path.join(tmp, 'locale-head.json');
const output = path.join(tmp, 'locale-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);
assert.equal(receipt.summary.presentationChanged, true);
assert.deepEqual(receipt.changes, { components: [], connections: [], boundaries: [] });
});
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',
output: 'boundary-mask-z-order.html',
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(/([\s\S]*?)<\/section>/)?.[1] || '';
const currentComponents = delta.indexOf('');
const currentNode = delta.indexOf('data-node-id="node"', currentComponents);
const phantomMask = delta.match(
/]*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(/]*\/>/)?.[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('provenance-only changes stay separate from graph changes and remain visible', () => {
const base = read(baseFixture);
const head = read(baseFixture);
base.meta.repository = {
url: 'https://github.com/example/one',
revision: 'a'.repeat(40),
link_mode: 'web',
};
head.meta.repository = {
url: 'https://github.com/example/one',
revision: 'b'.repeat(40),
provider: 'github',
link_mode: 'local-only',
};
const receipt = compareArchitecture(base, head, { baseVerified: true, headVerified: true });
assert.equal(receipt.summary.provenanceChanged, true);
assert.deepEqual(receipt.provenance, {
changedFields: ['/link_mode', '/provider', '/revision'],
base: { revision: 'a'.repeat(40), linkMode: 'web' },
head: { revision: 'b'.repeat(40), provider: 'github', linkMode: 'local-only' },
});
assert.deepEqual(receipt.changes, { components: [], connections: [], boundaries: [] });
assert.deepEqual(architectureDeltaChangeRows(receipt), []);
const html = renderDelta(receipt);
assert.match(html, /data-provenance-changed="true"/);
assert.match(html, /Repository provenance changed/);
assert.match(html, /\/revision: aaaaaaaa → bbbbbbbb/);
assert.match(html, /\/provider: automatic → github/);
assert.match(html, /\/link_mode: web → local-only/);
assert.match(html, /\/link_mode, \/provider, \/revision/);
assert.match(html, /No component, relationship, or boundary changes; repository provenance changed\./);
assert.doesNotMatch(html, /No authored architecture changes\./);
assert.match(html, /Overview · 0 authored graph changes · provenance changed/);
assert.match(html, /No graph changes · ' \+ provenanceSummary/);
assert.match(html, /const proofWidth = ctx\.measureText\(proofLine\)\.width/);
assert.match(html, /fitCanvasLine\(secondary, secondaryWidth\)/);
assert.doesNotMatch(html, /data-change-key="provenance/);
assert.deepEqual(validateArchitectureDeltaHtml(html, receipt), { ok: true, checksPassed: 10, checkCount: 10 });
const missingNotice = html.replace(/