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:
2026-09-30 12:56:12 +03:00
co-authored by Claude Opus 5.5
parent 22815a9940
commit 30e2faa38c
15 changed files with 458 additions and 24 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## 0.2.0 — 2026-09-30
- `.archify` files: Archify JSON diagram sources with their own extension. They open as JSON, preview with the same button and keys, report problems, and get completion from the schema of their `diagram_type`.
- Render to HTML and Open in Browser name the output after the diagram (`web.archify` → `web.html`).
- `examples/hello.archify` starter diagram.
## 0.1.0 — 2026-09-30
- Live preview for Archify JSON sources (architecture, workflow, sequence, dataflow, lifecycle), re-rendered as you type.
+34 -3
View File
@@ -1,15 +1,46 @@
# Archify Diagram Viewer
View [Archify](https://github.com/tt-a1i/archify) diagrams inside VS Code. Open a diagram's JSON source and see the interactive diagram next to it, updated as you type. Open rendered Archify HTML files in a viewer tab instead of a browser.
View [Archify](https://github.com/tt-a1i/archify) diagrams inside VS Code. Write a diagram in a `.archify` file and see the interactive diagram next to it, updated as you type. Open rendered Archify HTML files in a viewer tab instead of a browser.
This is an unofficial extension. It bundles the Archify renderer (MIT, © tt-a1i) and runs it on VS Code's own Node runtime, so nothing else needs to be installed.
## Quick start
1. Create a file ending in `.archify`, for example `hello.archify`:
```json
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": { "title": "Hello Archify", "output": "hello.html" },
"components": [
{ "id": "browser", "type": "frontend", "label": "Browser", "pos": [40, 120], "size": [140, 60] },
{ "id": "api", "type": "backend", "label": "API", "pos": [260, 120], "size": [140, 60] },
{ "id": "db", "type": "database", "label": "PostgreSQL", "pos": [480, 120], "size": [140, 60] }
],
"connections": [
{ "from": "browser", "to": "api", "label": "HTTPS" },
{ "from": "api", "to": "db", "label": "SQL" }
]
}
```
2. Click the preview button in the editor title, or press <kbd>Cmd/Ctrl+K V</kbd>.
The same file is in [examples/hello.archify](examples/hello.archify).
## The `.archify` format
A `.archify` file is an Archify JSON diagram source, the same JSON the Archify CLI renders. `diagram_type` selects the kind of diagram: `architecture`, `workflow`, `sequence`, `dataflow` or `lifecycle`. VS Code treats `.archify` as JSON, so you get syntax highlighting, formatting, and completion and hover from the schema for that `diagram_type`.
Archify's own naming, `name.architecture.json`, `name.workflow.json` and so on, works too.
## Features
- **Live preview**: for `*.architecture.json`, `*.workflow.json`, `*.sequence.json`, `*.dataflow.json` and `*.lifecycle.json` files, or any JSON with a `diagram_type`. Use the preview button in the editor title, <kbd>Cmd/Ctrl+K V</kbd> (to the side) or <kbd>Cmd/Ctrl+Shift+V</kbd>. The preview keeps the focus and route you selected (`#focus=…`, `#route=…`) when it re-renders.
- **Live preview**: for `.archify` files, Archify's `*.architecture.json`-style files, or any JSON with a `diagram_type`. Use the preview button in the editor title, <kbd>Cmd/Ctrl+K V</kbd> (to the side) or <kbd>Cmd/Ctrl+Shift+V</kbd>. The preview keeps the focus and route you selected (`#focus=…`, `#route=…`) when it re-renders.
- **HTML viewer**: right-click a rendered `.html` file and choose **Open in Archify Viewer**, or use *Reopen Editor With… → Archify Diagram Viewer*. For Archify HTML already open as text, a button appears in the editor title. The viewer reloads when the file changes on disk.
- **Problems**: `archify validate` runs on the source and reports schema, layout and evidence problems at the matching JSON location.
- **Schema help**: completion and hover documentation come from Archify's own JSON schemas.
- **Schema help**: completion and hover documentation come from Archify's own JSON schemas. In `.archify` files, the schema follows `diagram_type`.
- **Source links**: in source-backed diagrams, clicking a source reference opens that file and line range in the editor. If the file is not in the workspace, the web link opens instead.
- **Export**: the diagram's own Export menu (PNG, SVG, share card, …) saves through a VS Code save dialog.
- **Commands**: *Archify: Render to HTML File…*, *Archify: Open in Browser*, *Archify: Refresh Preview*, *Archify: Show Source*.
+17
View File
@@ -0,0 +1,17 @@
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": {
"title": "Hello Archify",
"output": "hello.html"
},
"components": [
{ "id": "browser", "type": "frontend", "label": "Browser", "pos": [40, 120], "size": [140, 60] },
{ "id": "api", "type": "backend", "label": "API", "pos": [260, 120], "size": [140, 60] },
{ "id": "db", "type": "database", "label": "PostgreSQL", "pos": [480, 120], "size": [140, 60] }
],
"connections": [
{ "from": "browser", "to": "api", "label": "HTTPS" },
{ "from": "api", "to": "db", "label": "SQL" }
]
}
+3 -2
View File
@@ -1,18 +1,19 @@
{
"name": "archify-vscode-ext",
"version": "0.1.0",
"version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "archify-vscode-ext",
"version": "0.1.0",
"version": "0.2.0",
"license": "MIT",
"devDependencies": {
"@types/node": "^22.0.0",
"@types/vscode": "~1.100.0",
"@vscode/test-electron": "^3.1.0",
"@vscode/vsce": "^3.0.0",
"ajv": "^8.20.0",
"esbuild": "^0.25.0",
"jsonc-parser": "^3.3.1",
"typescript": "^5.8.0"
+20 -7
View File
@@ -1,8 +1,8 @@
{
"name": "archify-vscode-ext",
"displayName": "Archify Diagram Viewer",
"description": "Preview Archify diagrams (architecture, workflow, sequence, dataflow, lifecycle) from their JSON sources and open rendered Archify HTML inside VS Code.",
"version": "0.1.0",
"description": "Visualize Archify diagrams: preview .archify files (and *.architecture.json-style sources) live, and open rendered Archify HTML inside VS Code.",
"version": "0.2.0",
"publisher": "vscode-extensions",
"license": "MIT",
"repository": {
@@ -28,7 +28,7 @@
"onLanguage:json",
"onLanguage:jsonc",
"onLanguage:html",
"workspaceContains:**/*.{architecture,workflow,sequence,dataflow,lifecycle}.json",
"workspaceContains:**/*.{archify,architecture.json,workflow.json,sequence.json,dataflow.json,lifecycle.json}",
"onWebviewPanel:archify.preview"
],
"contributes": {
@@ -53,7 +53,7 @@
},
{
"command": "archify.renderToHtml",
"title": "Render to HTML File\u2026",
"title": "Render to HTML File…",
"category": "Archify",
"icon": "$(file-code)"
},
@@ -142,12 +142,12 @@
"explorer/context": [
{
"command": "archify.showPreview",
"when": "resourceFilename =~ /\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json$/",
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
"group": "navigation@20"
},
{
"command": "archify.renderToHtml",
"when": "resourceFilename =~ /\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json$/",
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
"group": "navigation@21"
},
{
@@ -159,7 +159,7 @@
"editor/title/context": [
{
"command": "archify.showPreview",
"when": "resourceFilename =~ /\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json$/",
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
"group": "1_open"
}
]
@@ -191,6 +191,10 @@
}
],
"jsonValidation": [
{
"fileMatch": "*.archify",
"url": "./schemas/archify.schema.json"
},
{
"fileMatch": "*.architecture.json",
"url": "./schemas/architecture.schema.json"
@@ -287,7 +291,15 @@
"description": "Node.js executable (18 or newer) used to run the renderer. Empty means the runtime bundled with VS Code."
}
}
},
"languages": [
{
"id": "json",
"extensions": [
".archify"
]
}
]
},
"scripts": {
"sync:archify": "node scripts/sync-archify.mjs",
@@ -304,6 +316,7 @@
"@types/vscode": "~1.100.0",
"@vscode/test-electron": "^3.1.0",
"@vscode/vsce": "^3.0.0",
"ajv": "^8.20.0",
"esbuild": "^0.25.0",
"jsonc-parser": "^3.3.1",
"typescript": "^5.8.0"
+98
View File
@@ -0,0 +1,98 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Archify Diagram",
"description": "An Archify diagram source. diagram_type selects the schema that applies.",
"type": "object",
"required": [
"diagram_type"
],
"properties": {
"diagram_type": {
"enum": [
"architecture",
"workflow",
"sequence",
"dataflow",
"lifecycle"
],
"description": "Kind of diagram: architecture, workflow, sequence, dataflow or lifecycle."
}
},
"allOf": [
{
"if": {
"properties": {
"diagram_type": {
"const": "architecture"
}
},
"required": [
"diagram_type"
]
},
"then": {
"$ref": "architecture.schema.json"
}
},
{
"if": {
"properties": {
"diagram_type": {
"const": "workflow"
}
},
"required": [
"diagram_type"
]
},
"then": {
"$ref": "workflow.schema.json"
}
},
{
"if": {
"properties": {
"diagram_type": {
"const": "sequence"
}
},
"required": [
"diagram_type"
]
},
"then": {
"$ref": "sequence.schema.json"
}
},
{
"if": {
"properties": {
"diagram_type": {
"const": "dataflow"
}
},
"required": [
"diagram_type"
]
},
"then": {
"$ref": "dataflow.schema.json"
}
},
{
"if": {
"properties": {
"diagram_type": {
"const": "lifecycle"
}
},
"required": [
"diagram_type"
]
},
"then": {
"$ref": "lifecycle.schema.json"
}
}
]
}
+22
View File
@@ -60,6 +60,28 @@ for (const file of fs.readdirSync(path.join(skill, 'schemas'))) {
fs.writeFileSync(path.join(schemaOut, file), `${JSON.stringify(schema, null, 2)}\n`);
}
// *.archify files carry their type only in diagram_type, so they get one
// schema that dispatches to the per-type schema.
const TYPES = ['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'];
const dispatcher = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
title: 'Archify Diagram',
description: 'An Archify diagram source. diagram_type selects the schema that applies.',
type: 'object',
required: ['diagram_type'],
properties: {
diagram_type: {
enum: TYPES,
description: 'Kind of diagram: architecture, workflow, sequence, dataflow or lifecycle.',
},
},
allOf: TYPES.map((type) => ({
if: { properties: { diagram_type: { const: type } }, required: ['diagram_type'] },
then: { $ref: `${type}.schema.json` },
})),
};
fs.writeFileSync(path.join(schemaOut, 'archify.schema.json'), `${JSON.stringify(dispatcher, null, 2)}\n`);
let revision = 'unknown';
try {
revision = execFileSync('git', ['-C', source, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
+17
View File
@@ -13,6 +13,7 @@ export const EVIDENCE_COLLECTIONS: Record<DiagramType, string> = {
};
const FILENAME_RE = /\.(architecture|workflow|sequence|dataflow|lifecycle)\.json$/i;
const ARCHIFY_EXT_RE = /\.archify$/i;
const MAX_SNIFF_BYTES = 8 * 1024 * 1024;
export function isDiagramType(value: unknown): value is DiagramType {
@@ -24,6 +25,22 @@ export function typeFromFilename(fileName: string): DiagramType | undefined {
return match ? (match[1].toLowerCase() as DiagramType) : undefined;
}
/** `name.archify`: an Archify diagram whose type comes from `diagram_type`. */
export function isArchifyFile(fileName: string): boolean {
return ARCHIFY_EXT_RE.test(fileName);
}
/** File names that are diagram sources regardless of their content. */
export function isDiagramFileName(fileName: string): boolean {
return isArchifyFile(fileName) || FILENAME_RE.test(fileName);
}
/** Base name without the diagram source extension (`web.archify` → `web`). */
export function diagramBaseName(fileName: string): string {
const base = fileName.split(/[\\/]/).pop() ?? fileName;
return base.replace(ARCHIFY_EXT_RE, '').replace(/\.json$/i, '');
}
export interface DiagramSource {
type: DiagramType;
diagram: Record<string, unknown>;
+8 -6
View File
@@ -1,7 +1,7 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import * as vscode from 'vscode';
import { isArchifyHtml, readDiagramSource } from './detect';
import { diagramBaseName, isArchifyHtml, isDiagramFileName, readDiagramSource } from './detect';
import { DiagnosticsController } from './diagnostics';
import { HTML_VIEW_TYPE, HtmlViewerProvider } from './htmlViewer';
import { PreviewManager } from './preview';
@@ -35,7 +35,9 @@ export function activate(context: vscode.ExtensionContext): void {
let contextTimer: NodeJS.Timeout | undefined;
const updateContext = () => {
const doc = vscode.window.activeTextEditor?.document;
const diagram = Boolean(doc && JSON_LANGUAGES.has(doc.languageId) && readDiagramSource(doc.getText(), doc.fileName));
const diagram = Boolean(
doc && JSON_LANGUAGES.has(doc.languageId) && (isDiagramFileName(doc.fileName) || readDiagramSource(doc.getText(), doc.fileName)),
);
const html = Boolean(doc && doc.languageId === 'html' && isArchifyHtml(doc.getText()));
void vscode.commands.executeCommand('setContext', 'archify.isDiagramSource', diagram);
void vscode.commands.executeCommand('setContext', 'archify.isArchifyHtml', html);
@@ -96,12 +98,12 @@ export function activate(context: vscode.ExtensionContext): void {
const text = await readText(uri);
const source = readDiagramSource(text, uri.path);
if (!source) {
vscode.window.showWarningMessage(`Archify: ${path.basename(uri.path)} is not an Archify diagram source.`);
vscode.window.showWarningMessage(`Archify: ${path.basename(uri.path)} has no "diagram_type" (architecture, workflow, sequence, dataflow or lifecycle).`);
return;
}
const meta = (source.diagram.meta ?? {}) as { output?: unknown };
const authored = typeof meta.output === 'string' && !/[\\/]/.test(meta.output) ? meta.output : undefined;
const fileName = authored ?? `${path.basename(uri.path).replace(/\.json$/i, '')}.html`;
const fileName = authored ?? `${diagramBaseName(uri.path)}.html`;
const defaultUri = uri.scheme === 'file' ? vscode.Uri.joinPath(uri, '..', fileName) : undefined;
const target = await vscode.window.showSaveDialog({
defaultUri,
@@ -148,7 +150,7 @@ export function activate(context: vscode.ExtensionContext): void {
const text = await readText(uri);
const source = readDiagramSource(text, uri.path);
if (!source) {
vscode.window.showWarningMessage(`Archify: ${path.basename(uri.path)} is not an Archify diagram source.`);
vscode.window.showWarningMessage(`Archify: ${path.basename(uri.path)} has no "diagram_type" (architecture, workflow, sequence, dataflow or lifecycle).`);
return;
}
const runtime = await runtimeFor(context, uri);
@@ -162,7 +164,7 @@ export function activate(context: vscode.ExtensionContext): void {
}
const dir = path.join(context.globalStorageUri.fsPath, 'browser');
await fs.mkdir(dir, { recursive: true });
const file = path.join(dir, `${path.basename(uri.path).replace(/\.json$/i, '')}.html`);
const file = path.join(dir, `${diagramBaseName(uri.path)}.html`);
await fs.writeFile(file, result.html, 'utf8');
await vscode.env.openExternal(vscode.Uri.file(file));
}),
+4 -1
View File
@@ -92,7 +92,10 @@ class DiagramPreview implements vscode.Disposable {
}
const source = readDiagramSource(text, this.uri.path);
if (!source) {
this.showMessage('Not an Archify diagram', 'The file has no "diagram_type" of architecture, workflow, sequence, dataflow or lifecycle.');
this.showMessage(
'Nothing to show yet',
'Set "diagram_type" to architecture, workflow, sequence, dataflow or lifecycle, and make sure the file is valid JSON.',
);
return;
}
+12
View File
@@ -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);
});
+77
View File
@@ -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"
]
}
]
}
+51
View File
@@ -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"] }
]
}
+45 -4
View File
@@ -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');
+43
View File
@@ -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);
});