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(/