Add archify-vscode-ext skill: Archify diagrams and models for the Archify Diagram Viewer extension
Model tasks (validate, list, scaffold, resolve), diagram tasks (validate, render), the bundled Archify 3.0.1 runtime and yaml parser under scripts/vendor, references for authoring, formats and folders, and the online-shop example model.
This commit is contained in:
commit
b6b5e57922
226 files changed
+67520
No files matched your search
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
// The skill's own checks. Runs every action against examples/online-shop and
|
||||
// a scaffolded temp model. Prints YAML (success first); exit 1 on any failure.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { toYaml } from '../services/yaml.mjs';
|
||||
|
||||
const scripts = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const skill = path.resolve(scripts, '..');
|
||||
const example = path.join(skill, 'examples', 'online-shop');
|
||||
const cli = path.join(scripts, 'src', 'cli', 'commands.mjs');
|
||||
const checks = [];
|
||||
|
||||
function cliRun(args) {
|
||||
const result = spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8', cwd: scripts, maxBuffer: 64 * 1024 * 1024 });
|
||||
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
||||
}
|
||||
|
||||
function yamlValue(stdout, key) {
|
||||
const match = new RegExp(`^${key}: (.*)$`, 'm').exec(stdout);
|
||||
return match ? match[1].trim() : undefined;
|
||||
}
|
||||
|
||||
function check(name, command, expect) {
|
||||
const result = cliRun(command);
|
||||
let ok = true;
|
||||
const notes = [];
|
||||
try {
|
||||
expect(result, notes);
|
||||
} catch (error) {
|
||||
ok = false;
|
||||
notes.push(error.message);
|
||||
}
|
||||
checks.push({ name, command: `node src/cli/commands.mjs ${command.join(' ')}`, ok, notes, stdout: ok ? undefined : result.stdout.slice(0, 4000) });
|
||||
}
|
||||
|
||||
const assert = (condition, message) => {
|
||||
if (!condition) throw new Error(message);
|
||||
};
|
||||
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-skill-test-'));
|
||||
try {
|
||||
check('help prints usage', ['--help'], (r) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
assert(/model-validate/.test(r.stdout) && /diagram-render/.test(r.stdout), 'actions missing from --help');
|
||||
});
|
||||
|
||||
check('model-validate example: 4 diagrams, 0 problems', ['--action', 'model-validate', '--dir', example], (r, notes) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
assert(yamlValue(r.stdout, ' errors') === '0', 'errors are not 0');
|
||||
assert(yamlValue(r.stdout, ' warnings') === '0', 'warnings are not 0');
|
||||
assert(yamlValue(r.stdout, ' diagrams') === '4', 'expected 4 diagrams');
|
||||
assert(yamlValue(r.stdout, ' diagramsOk') === '4', 'expected 4 diagrams ok');
|
||||
notes.push('4 diagrams validated with 0 problems');
|
||||
});
|
||||
|
||||
check('model-validate from a file inside the model', ['--action', 'model-validate', '--dir', path.join(example, '0300-components', 'api.archify')], (r) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
assert(yamlValue(r.stdout, 'mode') === 'model', 'root was not found from a file inside');
|
||||
});
|
||||
|
||||
check('model-list example: 3 groups plus root-level main.archify', ['--action', 'model-list', '--dir', example], (r, notes) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
assert(yamlValue(r.stdout, ' groups') === '3', 'expected 3 groups');
|
||||
assert(/- file: main\.archify/.test(r.stdout), 'main.archify not listed at root level');
|
||||
assert(/folder: 0100-context/.test(r.stdout) && /folder: 0200-containers/.test(r.stdout) && /folder: 0300-components/.test(r.stdout), 'group folders missing');
|
||||
assert(/id: api\n(?:.*\n)*?\s+usedIn: 2/.test(r.stdout), 'api should be used in 2 diagrams');
|
||||
assert(yamlValue(r.stdout, ' elements') === '11', 'expected 11 registry elements');
|
||||
notes.push('3 groups, 11 elements with usage counts');
|
||||
});
|
||||
|
||||
const resolved = path.join(tmp, 'containers.architecture.json');
|
||||
check('model-resolve writes Archify JSON', ['--action', 'model-resolve', '--file', path.join(example, '0200-containers', 'online-shop.archify'), '--out', resolved], (r, notes) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
const json = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
||||
assert(json.model === undefined, 'model block leaked into the resolved JSON');
|
||||
const api = json.components.find((c) => c.id === 'api');
|
||||
assert(api && api.type === 'backend' && api.brand === 'node-js' && api.description === undefined && api.diagram === undefined, 'api was not resolved from the registry');
|
||||
notes.push(`${json.components.length} components, refs expanded`);
|
||||
});
|
||||
|
||||
check('model-resolve prints the JSON inline without --out', ['--action', 'model-resolve', '--file', path.join(example, 'main.archify')], (r) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
assert(/^resolved:/m.test(r.stdout) && /label: Online Shop/.test(r.stdout), 'resolved block missing');
|
||||
});
|
||||
|
||||
check('diagram-validate inside the model', ['--action', 'diagram-validate', '--file', path.join(example, '0100-context', 'online-shop.archify')], (r) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
assert(yamlValue(r.stdout, 'archifyOk') === 'true', 'Archify did not accept the diagram');
|
||||
});
|
||||
|
||||
const html = path.join(tmp, 'context.html');
|
||||
check('diagram-render writes HTML', ['--action', 'diagram-render', '--file', path.join(example, '0100-context', 'online-shop.archify'), '--out', html], (r, notes) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
assert(fs.existsSync(html) && fs.statSync(html).size > 10000, 'HTML missing or too small');
|
||||
assert(/<svg/.test(fs.readFileSync(html, 'utf8')), 'HTML has no SVG');
|
||||
notes.push(`${fs.statSync(html).size} bytes`);
|
||||
});
|
||||
|
||||
// Negative cases: a broken copy of the example.
|
||||
const broken = path.join(tmp, 'broken');
|
||||
fs.cpSync(example, broken, { recursive: true });
|
||||
fs.appendFileSync(path.join(broken, 'registry', 'people.archify-registry'), ' api:\n type: backend\n label: Dup\n');
|
||||
fs.writeFileSync(
|
||||
path.join(broken, '0300-components', 'bad.archify'),
|
||||
'schema_version: 1\ndiagram_type: architecture\nmeta:\n title: Bad\n output: bad.html\ncomponents:\n - ref: nobody\n pos: [40, 60]\n size: [130, 60]\n - ref: db\n type: frontend\n pos: [300, 60]\n size: [130, 60]\n diagram: 0900-missing/x.archify\n - id: a\n type: backend\n label: A\n pos: [40, 200]\n size: [130, 60]\n - id: a\n type: backend\n label: A again\n pos: [40, 200]\n size: [130, 60]\n',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(broken, '0300-components', 'overlap.archify'),
|
||||
'schema_version: 1\ndiagram_type: architecture\nmeta:\n title: Overlap\n output: overlap.html\ncomponents:\n - ref: api\n pos: [40, 60]\n size: [130, 60]\n - ref: db\n pos: [60, 70]\n size: [130, 60]\n',
|
||||
);
|
||||
check('model-validate reports duplicates, unknown refs, overrides, links, ids, layout', ['--action', 'model-validate', '--dir', broken], (r, notes) => {
|
||||
assert(r.status === 1, `exit ${r.status}, expected 1`);
|
||||
const text = r.stdout.replace(/\\"/g, '"');
|
||||
for (const expected of ['defined twice', 'Unknown element "nobody"', '"type" of shared element "db"', 'does not exist under', 'Duplicate id "a"', 'less than 8px apart']) {
|
||||
assert(text.includes(expected), `missing problem: ${expected}`);
|
||||
}
|
||||
assert(/pointer: \/components\/0\/ref/.test(text) && /line: 7/.test(text), 'pointer/line of the unknown ref missing');
|
||||
assert(/file: 0300-components\/overlap\.archify\n(?:.*\n)*?\s+line: 7\n\s+pointer: \/components\/0\n\s+code: layout\/constraint/.test(text), 'Archify layout problem not mapped to a component line');
|
||||
notes.push('all expected problems reported with file, line and pointer');
|
||||
});
|
||||
|
||||
const single = path.join(tmp, 'single.archify');
|
||||
fs.writeFileSync(single, 'schema_version: 1\ndiagram_type: architecture\nmeta:\n title: Single\n output: single.html\ncomponents:\n - { id: a, type: backend, label: A, pos: [40, 60], size: [130, 60] }\n - { id: b, type: database, label: B, pos: [300, 60], size: [130, 60] }\nconnections:\n - { from: a, to: b, label: SQL }\n');
|
||||
check('diagram outside a model validates with Archify only', ['--action', 'model-validate', '--dir', single], (r) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
assert(yamlValue(r.stdout, 'mode') === 'single-diagram', 'expected single-diagram mode');
|
||||
});
|
||||
|
||||
const outside = path.join(tmp, 'outside.archify');
|
||||
fs.writeFileSync(outside, 'schema_version: 1\ndiagram_type: architecture\nmeta:\n title: Outside\n output: outside.html\ncomponents:\n - { ref: api, pos: [40, 60], size: [130, 60] }\n');
|
||||
check('ref outside a model is an error', ['--action', 'diagram-validate', '--file', outside], (r) => {
|
||||
assert(r.status === 1, `exit ${r.status}, expected 1`);
|
||||
assert(/cannot be resolved: no main\.archify above/.test(r.stdout), 'missing "no main.archify" problem');
|
||||
});
|
||||
|
||||
const scaffold = path.join(tmp, 'scaffolded');
|
||||
check('model-scaffold --c4 creates a model', ['--action', 'model-scaffold', '--dir', scaffold, '--id', 'demo-shop', '--name', 'Demo Shop', '--description', 'A demo: scaffolded model', '--c4'], (r, notes) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
for (const file of ['main.archify', 'registry/people.archify-registry', 'registry/systems.archify-registry', 'registry/containers.archify-registry', '0100-context/demo-shop.archify', '0200-containers/demo-shop.archify', '0300-components/api.archify']) {
|
||||
assert(fs.existsSync(path.join(scaffold, file)), `${file} not created`);
|
||||
}
|
||||
notes.push('7 files created');
|
||||
});
|
||||
check('scaffolded model validates', ['--action', 'model-validate', '--dir', scaffold], (r) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
assert(yamlValue(r.stdout, ' errors') === '0' && yamlValue(r.stdout, ' warnings') === '0', 'scaffolded model has problems');
|
||||
assert(yamlValue(r.stdout, ' diagrams') === '4', 'expected 4 diagrams in the scaffolded model');
|
||||
});
|
||||
check('scaffolded model lists 3 groups', ['--action', 'model-list', '--dir', scaffold], (r) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
assert(yamlValue(r.stdout, ' groups') === '3', 'expected 3 groups');
|
||||
});
|
||||
check('model-scaffold refuses to overwrite', ['--action', 'model-scaffold', '--dir', scaffold, '--id', 'demo-shop', '--name', 'Demo Shop', '--c4'], (r) => {
|
||||
assert(r.status === 1, `exit ${r.status}, expected 1`);
|
||||
assert(/Refusing to overwrite/.test(r.stdout), 'no refusal message');
|
||||
assert(fs.readFileSync(path.join(scaffold, 'main.archify'), 'utf8').includes('Demo Shop'), 'main.archify was touched');
|
||||
});
|
||||
const plain = path.join(tmp, 'plain');
|
||||
check('model-scaffold without --c4 creates main.archify only', ['--action', 'model-scaffold', '--dir', plain, '--id', 'plain', '--name', 'Plain'], (r) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
assert(fs.readdirSync(plain).join(',') === 'main.archify', `unexpected files: ${fs.readdirSync(plain).join(',')}`);
|
||||
});
|
||||
check('plain model validates (model block only)', ['--action', 'model-validate', '--dir', plain], (r) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const failed = checks.filter((item) => !item.ok).length;
|
||||
process.stdout.write(toYaml({ success: failed === 0, summary: { checks: checks.length, passed: checks.length - failed, failed }, checks }));
|
||||
process.exit(failed ? 1 : 0);
|
||||
Reference in new issue
Block a user