diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0507547..6a50d99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/README.md b/README.md
index 2d26585..2907c25 100644
--- a/README.md
+++ b/README.md
@@ -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 Cmd/Ctrl+K V.
+
+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, Cmd/Ctrl+K V (to the side) or Cmd/Ctrl+Shift+V. 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, Cmd/Ctrl+K V (to the side) or Cmd/Ctrl+Shift+V. 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*.
diff --git a/examples/hello.archify b/examples/hello.archify
new file mode 100644
index 0000000..5e8c705
--- /dev/null
+++ b/examples/hello.archify
@@ -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" }
+ ]
+}
diff --git a/package-lock.json b/package-lock.json
index 7cc27d7..582418f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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"
diff --git a/package.json b/package.json
index ad5153d..4924e32 100644
--- a/package.json
+++ b/package.json
@@ -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"
diff --git a/schemas/archify.schema.json b/schemas/archify.schema.json
new file mode 100644
index 0000000..02a372d
--- /dev/null
+++ b/schemas/archify.schema.json
@@ -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"
+ }
+ }
+ ]
+}
diff --git a/scripts/sync-archify.mjs b/scripts/sync-archify.mjs
index 024b24e..305ab73 100644
--- a/scripts/sync-archify.mjs
+++ b/scripts/sync-archify.mjs
@@ -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();
diff --git a/src/detect.ts b/src/detect.ts
index 95ce4db..ca46195 100644
--- a/src/detect.ts
+++ b/src/detect.ts
@@ -13,6 +13,7 @@ export const EVIDENCE_COLLECTIONS: Record = {
};
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;
diff --git a/src/extension.ts b/src/extension.ts
index 580e5f4..43fff4f 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -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));
}),
diff --git a/src/preview.ts b/src/preview.ts
index b4ed062..96bd634 100644
--- a/src/preview.ts
+++ b/src/preview.ts
@@ -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;
}
diff --git a/test/detect.test.ts b/test/detect.test.ts
index 97f8127..e161053 100644
--- a/test/detect.test.ts
+++ b/test/detect.test.ts
@@ -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);
+});
diff --git a/test/fixtures/cache-miss.archify b/test/fixtures/cache-miss.archify
new file mode 100644
index 0000000..4ef2fab
--- /dev/null
+++ b/test/fixtures/cache-miss.archify
@@ -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"
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/web-platform.archify b/test/fixtures/web-platform.archify
new file mode 100644
index 0000000..bbcf9bd
--- /dev/null
+++ b/test/fixtures/web-platform.archify
@@ -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"] }
+ ]
+}
diff --git a/test/integration/suite.ts b/test/integration/suite.ts
index 6dbec80..23c38ec 100644
--- a/test/integration/suite.ts
+++ b/test/integration/suite.ts
@@ -33,7 +33,7 @@ async function check(name: string, body: () => Promise): Promise {
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 {
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.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 {
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');
diff --git a/test/schema.test.ts b/test/schema.test.ts
new file mode 100644
index 0000000..c2a8e50
--- /dev/null
+++ b/test/schema.test.ts
@@ -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);
+});