Files
archify-vscode-ext/src/detect.ts
T
root-at-skicandClaude Opus 5.5 30e2faa38c 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>
2026-09-30 12:56:12 +03:00

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;
}