.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>
95 lines
3.7 KiB
TypeScript
95 lines
3.7 KiB
TypeScript
import { parse, ParseError } from 'jsonc-parser';
|
|
|
|
export const DIAGRAM_TYPES = ['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'] as const;
|
|
export type DiagramType = (typeof DIAGRAM_TYPES)[number];
|
|
|
|
/** Node collection that may carry `sources` evidence, per diagram type. */
|
|
export const EVIDENCE_COLLECTIONS: Record<DiagramType, string> = {
|
|
architecture: 'components',
|
|
workflow: 'nodes',
|
|
sequence: 'participants',
|
|
dataflow: 'nodes',
|
|
lifecycle: 'states',
|
|
};
|
|
|
|
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 {
|
|
return typeof value === 'string' && (DIAGRAM_TYPES as readonly string[]).includes(value);
|
|
}
|
|
|
|
export function typeFromFilename(fileName: string): DiagramType | undefined {
|
|
const match = FILENAME_RE.exec(fileName);
|
|
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>;
|
|
}
|
|
|
|
/**
|
|
* Parses JSON text and returns the diagram when it declares an Archify
|
|
* `diagram_type`. Falls back to the file name for documents that do not
|
|
* parse yet, so a half-typed source still counts as a diagram.
|
|
*/
|
|
export function readDiagramSource(text: string, fileName = ''): DiagramSource | undefined {
|
|
if (text.length > MAX_SNIFF_BYTES) return undefined;
|
|
const errors: ParseError[] = [];
|
|
const value = parse(text, errors, { allowTrailingComma: true });
|
|
if (value && typeof value === 'object' && !Array.isArray(value) && isDiagramType(value.diagram_type)) {
|
|
return { type: value.diagram_type, diagram: value };
|
|
}
|
|
const byName = typeFromFilename(fileName);
|
|
if (byName) {
|
|
const diagram = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
return { type: byName, diagram };
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** True when the HTML was produced by the Archify renderer. */
|
|
export function isArchifyHtml(text: string): boolean {
|
|
const head = text.slice(0, 4096);
|
|
return /<meta\s+name=["']generator["']\s+content=["']archify\b/i.test(head);
|
|
}
|
|
|
|
export function hasSourceEvidence(type: DiagramType, diagram: Record<string, unknown>): boolean {
|
|
const meta = diagram.meta as Record<string, unknown> | undefined;
|
|
if (meta && meta.repository) return true;
|
|
const nodes = diagram[EVIDENCE_COLLECTIONS[type]];
|
|
return Array.isArray(nodes) && nodes.some((node) => Array.isArray(node?.sources) && node.sources.length > 0);
|
|
}
|
|
|
|
/** Returns a copy of the diagram with repository evidence removed. */
|
|
export function stripSourceEvidence(type: DiagramType, diagram: Record<string, unknown>): Record<string, unknown> {
|
|
const copy = structuredClone(diagram);
|
|
const meta = copy.meta as Record<string, unknown> | undefined;
|
|
if (meta) delete meta.repository;
|
|
const nodes = copy[EVIDENCE_COLLECTIONS[type]];
|
|
if (Array.isArray(nodes)) {
|
|
for (const node of nodes) {
|
|
if (node && typeof node === 'object') delete node.sources;
|
|
}
|
|
}
|
|
return copy;
|
|
}
|