Support .archify diagram files (0.2.0)
.archify files are Archify JSON diagram sources. They open as JSON, preview with the same commands, report validation problems, and get completion from a schema that dispatches on diagram_type. Adds a hello.archify example and tests for detection, the dispatching schema and the VS Code behaviour. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,3 +47,15 @@ test('json pointer ranges point at keys for containers and values for leaves', (
|
||||
const missing = rangeForPointer(text, '/components/0/label');
|
||||
assert.equal(text.slice(missing.start, missing.end), '{', 'falls back to the nearest existing ancestor');
|
||||
});
|
||||
|
||||
test('.archify files are diagram sources typed by their content', async () => {
|
||||
const { diagramBaseName, isArchifyFile, isDiagramFileName } = await import('../src/detect');
|
||||
assert.ok(isArchifyFile('/w/My Platform.archify'));
|
||||
assert.ok(isDiagramFileName('/w/x.ARCHIFY'));
|
||||
assert.ok(isDiagramFileName('/w/x.workflow.json'));
|
||||
assert.ok(!isDiagramFileName('/w/archify.json'));
|
||||
assert.equal(diagramBaseName('/w/web.archify'), 'web');
|
||||
assert.equal(diagramBaseName('/w/web.architecture.json'), 'web.architecture');
|
||||
assert.equal(readDiagramSource('{"diagram_type":"lifecycle"}', 'run.archify')?.type, 'lifecycle');
|
||||
assert.equal(readDiagramSource('{"title":"no type yet"}', 'run.archify'), undefined);
|
||||
});
|
||||
|
||||
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"diagram_type": "sequence",
|
||||
"meta": {
|
||||
"title": "Cache Miss Request Sequence",
|
||||
"output": "examples/sequence-cache-miss-request.html",
|
||||
"viewBox": [1080, 580],
|
||||
"column_fit": "spread",
|
||||
"animation": "trace",
|
||||
"quality_profile": "showcase"
|
||||
},
|
||||
"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": 250, "label": "Request" },
|
||||
{ "from": 260, "to": 370, "label": "Fallback" },
|
||||
{ "from": 380, "to": 480, "label": "Response + trace" }
|
||||
],
|
||||
"messages": [
|
||||
{ "id": "open-page", "from": "user", "to": "web", "y": 160, "label": "open page", "variant": "default" },
|
||||
{ "id": "dashboard-request", "from": "web", "to": "api", "y": 185, "label": "GET /dashboard", "variant": "emphasis" },
|
||||
{ "id": "verify-jwt", "from": "api", "to": "auth", "y": 210, "label": "verify JWT", "variant": "security" },
|
||||
{ "id": "auth-claims", "from": "auth", "to": "api", "y": 238, "label": "claims ok", "variant": "return" },
|
||||
{ "id": "cache-read", "from": "api", "to": "redis", "y": 270, "label": "read cache", "variant": "default" },
|
||||
{ "id": "cache-miss", "from": "redis", "to": "api", "y": 298, "label": "miss", "variant": "return" },
|
||||
{ "id": "profile-query", "from": "api", "to": "db", "y": 330, "label": "query profile + metrics", "variant": "emphasis" },
|
||||
{ "id": "profile-rows", "from": "db", "to": "api", "y": 358, "label": "rows", "variant": "return" },
|
||||
{ "id": "cache-write", "from": "api", "to": "redis", "y": 390, "label": "set cache", "variant": "dashed" },
|
||||
{ "id": "trace-emit", "from": "api", "to": "trace", "y": 418, "label": "emit trace", "variant": "dashed" },
|
||||
{ "id": "dashboard-response", "from": "api", "to": "web", "y": 443, "label": "200 JSON", "variant": "return" },
|
||||
{ "id": "page-render", "from": "web", "to": "user", "y": 468, "label": "render", "variant": "return" }
|
||||
],
|
||||
"activations": [
|
||||
{ "participant": "web", "from": 180, "to": 474, "type": "frontend" },
|
||||
{ "participant": "api", "from": 185, "to": 450, "type": "backend" },
|
||||
{ "participant": "auth", "from": 205, "to": 244, "type": "security" },
|
||||
{ "participant": "redis", "from": 265, "to": 304, "type": "database" },
|
||||
{ "participant": "db", "from": 325, "to": 364, "type": "database" },
|
||||
{ "participant": "trace", "from": 413, "to": 449, "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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"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": [1190, 300], "size": [126, 60], "tag": "app team" },
|
||||
{ "id": "replica", "type": "database", "label": "DR Replica", "sublabel": "eu-west-1", "pos": [1040, 578], "size": [126, 62], "tag": "data team" },
|
||||
{ "id": "audit", "type": "cloud", "label": "Audit Archive", "sublabel": "immutable objects", "pos": [1190, 450], "size": [126, 62], "tag": "security" },
|
||||
{ "id": "observability", "type": "external", "label": "Observability", "sublabel": "metrics + traces", "pos": [1190, 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"], "pad": 20 },
|
||||
{ "kind": "security-group", "label": "private application network", "wraps": ["api_a", "api_b", "redis", "postgres", "events", "worker"], "pad": 14 },
|
||||
{ "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"] }
|
||||
]
|
||||
}
|
||||
@@ -33,7 +33,7 @@ async function check(name: string, body: () => Promise<void>): Promise<void> {
|
||||
await body();
|
||||
console.log(` ✔ ${name}`);
|
||||
} catch (error) {
|
||||
console.log(` ✖ ${name}`);
|
||||
console.log(` ✖ ${name}\n ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -49,10 +49,51 @@ export async function run(): Promise<void> {
|
||||
await waitFor('activation', () => extension.isActive);
|
||||
});
|
||||
|
||||
await check('opens a live preview beside the source', async () => {
|
||||
await check('.archify files open as JSON with the preview available', async () => {
|
||||
const doc = await vscode.workspace.openTextDocument(workspaceFile('web-platform.archify'));
|
||||
assert.equal(doc.languageId, 'json');
|
||||
await vscode.window.showTextDocument(doc);
|
||||
await vscode.commands.executeCommand('archify.showPreviewToSide');
|
||||
const tabs = await waitFor('.archify preview tab', () => {
|
||||
const found = webviewTabs('archify.preview').filter((tab) => /web-platform\.archify/.test(tab.label));
|
||||
return found.length > 0 && found;
|
||||
});
|
||||
assert.equal(tabs.length, 1);
|
||||
});
|
||||
|
||||
await check('.archify completion comes from the schema of its diagram_type', async () => {
|
||||
const doc = await vscode.workspace.openTextDocument(workspaceFile('web-platform.archify'));
|
||||
await vscode.window.showTextDocument(doc);
|
||||
// Complete the value of the first component's "type".
|
||||
const offset = doc.getText().indexOf('"type": "') + '"type": "'.length;
|
||||
const position = doc.positionAt(offset);
|
||||
let labels: string[] = [];
|
||||
for (let attempt = 0; attempt < 50 && !labels.includes('backend'); attempt += 1) {
|
||||
const list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', doc.uri, position);
|
||||
labels = list.items.map((item) => (typeof item.label === 'string' ? item.label : item.label.label).replace(/"/g, ''));
|
||||
if (!labels.includes('backend')) await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
}
|
||||
assert.ok(labels.includes('backend') && labels.includes('database'), `offered: ${labels.join(', ')}`);
|
||||
});
|
||||
|
||||
await check('.archify problems are reported against the file', async () => {
|
||||
const good = await vscode.workspace.openTextDocument(workspaceFile('cache-miss.archify'));
|
||||
const broken = good.getText().replace('"participants"', '"actors"');
|
||||
const target = workspaceFile('broken.archify');
|
||||
await vscode.workspace.fs.writeFile(target, new TextEncoder().encode(broken));
|
||||
const doc = await vscode.workspace.openTextDocument(target);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
const diagnostics = await waitFor('archify diagnostics on broken.archify', () => {
|
||||
const found = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify');
|
||||
return found.length > 0 && found;
|
||||
});
|
||||
assert.ok(diagnostics.some((d) => d.severity === vscode.DiagnosticSeverity.Error));
|
||||
});
|
||||
|
||||
await check('opens a live preview beside the source', async () => {
|
||||
await vscode.commands.executeCommand('archify.showPreviewToSide', workspaceFile('production-deployment.architecture.json'));
|
||||
const tabs = await waitFor('preview tab', () => {
|
||||
const found = webviewTabs('archify.preview');
|
||||
const found = webviewTabs('archify.preview').filter((tab) => /production-deployment/.test(tab.label));
|
||||
return found.length > 0 && found;
|
||||
});
|
||||
assert.equal(tabs.length, 1);
|
||||
@@ -107,7 +148,7 @@ export async function run(): Promise<void> {
|
||||
await check('source-backed diagram previews with the evidence fallback', async () => {
|
||||
const uri = workspaceFile('mco-runtime.architecture.json');
|
||||
await vscode.commands.executeCommand('archify.showPreview', uri);
|
||||
await waitFor('second preview tab', () => webviewTabs('archify.preview').length >= 2);
|
||||
await waitFor('mco preview tab', () => webviewTabs('archify.preview').some((tab) => /mco-runtime/.test(tab.label)));
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const diagnostics = await waitFor('evidence diagnostics', () => {
|
||||
const found = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify');
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { test } from 'node:test';
|
||||
import Ajv2020 from 'ajv/dist/2020';
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const schemas = path.join(root, 'schemas');
|
||||
const fixtures = path.join(root, 'test', 'fixtures');
|
||||
|
||||
function validator() {
|
||||
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
||||
// Register every schema under its file name, exactly how the relative
|
||||
// $refs in the bundled copies resolve inside VS Code.
|
||||
for (const file of fs.readdirSync(schemas)) {
|
||||
ajv.addSchema(JSON.parse(fs.readFileSync(path.join(schemas, file), 'utf8')), file);
|
||||
}
|
||||
return ajv.getSchema('archify.schema.json')!;
|
||||
}
|
||||
|
||||
test('bundled schemas carry no absolute $id', () => {
|
||||
for (const file of fs.readdirSync(schemas)) {
|
||||
assert.equal(JSON.parse(fs.readFileSync(path.join(schemas, file), 'utf8')).$id, undefined, file);
|
||||
}
|
||||
});
|
||||
|
||||
test('the .archify schema accepts every fixture', () => {
|
||||
const validate = validator();
|
||||
for (const file of fs.readdirSync(fixtures)) {
|
||||
const ok = validate(JSON.parse(fs.readFileSync(path.join(fixtures, file), 'utf8')));
|
||||
assert.ok(ok, `${file}: ${JSON.stringify(validate.errors?.slice(0, 3))}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the .archify schema applies the schema of the declared type', () => {
|
||||
const validate = validator();
|
||||
const sequence = JSON.parse(fs.readFileSync(path.join(fixtures, 'cache-miss.archify'), 'utf8'));
|
||||
// A sequence diagram is not a valid architecture diagram.
|
||||
assert.equal(validate({ ...sequence, diagram_type: 'architecture' }), false);
|
||||
assert.equal(validate({ ...sequence, diagram_type: 'poster' }), false);
|
||||
const { diagram_type: _omitted, ...untyped } = sequence;
|
||||
assert.equal(validate(untyped), false);
|
||||
});
|
||||
Reference in New Issue
Block a user