Перейти к содержимому

24. Библиотечные API

CLI, расширения редактора и Viewer построены на опубликованных пакетах: @archlang/engine (лексер, парсер, резолвер, каскад, граф зависимостей, дифф, рендер описаний и слой вычисления селекторов/политик/проекций v0.10), @archlang/lsp (загрузчик пакетов, языковой сервер, форматтер исходника) и @archlang/render (headless-растеризация SVG → PNG). Если вы строите что-то новое — свой валидатор, CI-бота, генератор документации, интеграцию для платформы не на JS — вы будете напрямую использовать один или несколько из этих пакетов.

Эта глава для авторов инструментов. Предполагается знакомство с JavaScript / TypeScript и Node.js. Если вы потребляете .arch файлы только через редактор и CLI, переходите к Главе 25.

ПакетЧто даётКогда использовать
@archlang/engineТекст исходника → AST → разрешённая модель → граф/дифф/описания/governanceНужен язык целиком, от сырого текста до governed, диффуемой модели
@archlang/lspЗагрузчик пакетов + языковой сервер + хелпер форматированияНужна загрузка из файловой системы, сервер для редактора или каноническое форматирование
@archlang/renderHeadless-растеризация SVG → PNG (resvg + встроенные шрифты)Нужны изображения диаграмм вне браузера — артефакты CI, сборка документации, инструменты MCP

lsp зависит от engine. render независим — он растеризует SVG-текст, произведённый чем-то другим (CLI, toSVG из @archlang/scene, или Viewer’ом); сам он никогда не парсит .arch. Выбирайте пакет, подходящий под задачу.

Стоит знать о границе пакетов: loadPackage живёт в @archlang/lsp, а не в engine. Он обрабатывает обход файловой системы и разрешение зависимостей, что важно только когда есть рабочее пространство для чтения. Чистый анализ уже распарсенных исходников использует только engine.

Окно терминала
npm install @archlang/engine

engine — это весь язык целиком: синтаксис (лексер + парсер) и семантика (резолвер, каскад, граф, дифф, рендер описаний, вычисление governance v0.10) в одном пакете. На нём строится всё вышестоящее.

import { parse } from "@archlang/engine";
const source = `
service #p7f3qa Payments {
aspect team: "Payments"
repo.url: "https://github.com/acme/payments"
spec.url: "https://specs.acme.internal/openapi/payments.yaml" // link out to the real contract
rest_create authorize
}
`;
const result = parse(source, "/main.arch");
// result.file — ParsedFile (declarations, uses, manifest if any)
// result.tokens — Token[] (the full lexer stream)
// result.errors — ParseError[] (syntax errors, each with an optional span + message)

parse(source: string, file?: string) — чистая функция из текста исходника в AST распарсенного файла плюс его ошибки лексера/парсера. Необязательный параметр file записывается на каждый span AST, чтобы вышестоящий инструментарий мог находить ошибки. Никакого разрешения имён, никакой валидации сверх синтаксической корректности, никакой межфайловой работы.

Типичные применения:

  • Подсветка синтаксиса в среде без LSP.
  • Инструменты поиска по строке, обходящие исходник в поиске деклараций, соответствующих шаблону.
  • Миграционные скрипты, переписывающие текст исходника — парсят, чтобы найти позиции, затем патчат исходник напрямую.

Типы AST экспортируются из пакета: ModuleDecl, InterfaceDecl, SurfaceDecl, ProcessDecl, ViewDeclV2, PolicyDecl, TypeDecl, SubprocessDecl и сопутствующие типы span / значений / деклараций.

import { resolvePackage, type PackageInput, type PackageMap } from "@archlang/engine";
const result = resolvePackage(input, deps);
// result.model — ResolvedModel (canonical resolved view)
// result.diagnostics — Diagnostic[] (resolution diagnostics)

resolvePackage(pkg: PackageInput, deps: PackageMap) принимает распарсенный пакет (его манифест плюс распарсенные файлы) и карту его зависимостей. Он применяет штамповку по шаблонам типов (Глава 15) и выдаёт ResolvedModel. Структурный каскад (Глава 18) — отдельный, явный проход: вызовите applyCascade(model) после этого, чтобы получить полностью прокаскадированную модель, которую ожидают валидация, граф и вычисление governance:

import { resolvePackage, applyCascade } from "@archlang/engine";
const { model } = resolvePackage(input, deps);
const cascaded = applyCascade(model);

ResolvedModel предоставляет массивы разрешённых модулей, поверхностей, интерфейсов, процессов и проекций:

import type { ResolvedModel, ResolvedModule } from "@archlang/engine";
function totalInterfaces(model: ResolvedModel): number {
return model.modules.reduce((sum, m) => sum + m.interfaces.length, 0);
}
import { validate } from "@archlang/engine";
const result = validate(cascaded);
// result.diagnostics — array of validator diagnostics

validate выдаёт каждую диагностику, которую может породить валидатор: пропущенные обязательные поля, неверные ссылки в шагах процесса, нарушения инвариантов аспектов, провалы межпакетных ссылок. Коды стабильны; уровни важности могут быть переопределены конфигом проекта.

import { buildGraph } from "@archlang/engine";
const graph = buildGraph(cascaded);
// graph.edges — DependencyEdge[] — one per derived call
// graph.modulesById — ReadonlyMap<StableId, ResolvedModule>
// graph.modulesByPath — ReadonlyMap<string, ResolvedModule>

Каждое ребро графа берётся из шага процесса. На графе работают проекции, анализ радиуса распространения (blast radius) и отчёты о влиянии.

Полезные хелперы в том же модуле — каждый принимает граф плюс ResolvedModule, а не голую строку идентификатора:

import { getDependents, getDependencies, computeBlastRadius, stableId } from "@archlang/engine";
const payments = graph.modulesById.get(stableId("p7f3qa"))!; // IDs are opaque + permanent
const dependents = getDependents(graph, payments);
const dependencies = getDependencies(graph, payments);
const blastRadius = computeBlastRadius(graph, payments);
import { diffModels, type ChangeSet, type ModuleDiff } from "@archlang/engine";
const delta: ChangeSet = diffModels(beforeModel, afterModel);
// delta.modules — ReadonlyMap<ModuleId, ModuleDiff>
// delta.processes — ReadonlyMap<ProcessId, ProcessDiff>
// delta.views — ReadonlyMap<ViewId, ViewDiff>

Каждый ModuleDiff несёт status (одно из added | removed | modified | renamed | unchanged), name (его текущее/итоговое имя), nameChange: { from, to } при переименовании, плюс изменения по каждому полю, аспекту, поверхности, интерфейсу. Итерация:

for (const [id, mDiff] of delta.modules) {
if (mDiff.status === "renamed" && mDiff.nameChange) {
console.log(`Renamed: ${mDiff.nameChange.from}${mDiff.nameChange.to} (${id})`);
} else if (mDiff.status === "added") {
console.log(`Added: ${mDiff.name} (${id})`);
} else if (mDiff.status === "removed") {
console.log(`Removed: ${mDiff.name} (${id})`);
}
}

Это движок, который использует режим диффа Viewer (Глава 14). Конвейеры, которым нужны машиночитаемые архитектурные дельты, потребляют diffModels напрямую. Обе модели должны быть прокаскадированы (applyCascade) перед сравнением, чтобы изменения полей/аспектов отражали действующие (пост-распространение) значения, которые фактически видит читатель.

import {
buildDescriptionIndex,
makeDescriptionContext,
renderDescription,
aspectLookup,
fieldLookup,
} from "@archlang/engine";
const index = buildDescriptionIndex(cascaded); // build once per model, reuse per description
const ctx = makeDescriptionContext(
index,
aspectLookup(somePaymentsModule.body.aspects), // resolves bare `@@team` interpolation
undefined, // referrerSpace — the space the description lives in
fieldLookup(somePaymentsModule.body.fields), // resolves bare `@field` interpolation
);
const raw = somePaymentsModule.body.descriptions.map((d) => d.text).join("\n\n");
const rendered = renderDescription(raw, ctx);
// rendered.markdown — refs (`[[X]]`) resolved to links, aspects/fields interpolated
// rendered.issues — DescriptionIssue[] (unresolved refs, etc.)

renderDescription — тот же рендерер, который LSP использует для всплывающих подсказок, а Viewer — для тултипов. buildDescriptionIndex один раз обходит разрешённую модель, индексируя каждую именованную декларацию, чтобы разрешались перекрёстные ссылки [[ref]] и внешние геттеры ([[X]]@@team, [[X]]@field); makeDescriptionContext связывает этот индекс с аксессорами аспектов/полей владеющего узла, чтобы корректно разрешалась голая интерполяция @@/@. Передавайте () => undefined для любого из аксессоров, когда в описании нечего разрешать через голую интерполяцию (процессы, подпроцессы и проекции хранят плоскую строку описания без собственных аспектов/полей). Используйте их при сборке страниц документации, которым нужен паритет с отображением в редакторе.

Селекторы, политики и проекции (governance API v0.10)

Заголовок раздела «Селекторы, политики и проекции (governance API v0.10)»

Слой запросов v0.10 — селекторы view/policy/query (Глава 8, Глава 28) — реализован как набор чистых вычислителей (evaluators) над ResolvedModel. На нём построен archlang policy-check; автор инструмента обращается к нему напрямую, когда policy-check не подходит по форме — свой governance-дашборд, pre-merge бот, которому нужны структурированные находки вместо кодов выхода, или панель редактора, подсвечивающая совпавшие узлы вживую.

Каждый вычислитель разделяет один контекст: вселенную узлов (node universe, каждый элемент, с ключом и доступный для поиска) плюс граф зависимостей и его замыкание по расстоянию (distance closure, для стрелок селектора >> / within):

import { buildNodeUniverse, buildGraph, buildClosure, type EvalContext } from "@archlang/engine";
const graph = buildGraph(cascaded);
const ctx: EvalContext = {
universe: buildNodeUniverse(cascaded),
graph,
closure: buildClosure(graph),
};

Вычисление селектора. parseSelectorSource превращает текст селектора в AST; evalSelector выполняет его над контекстом:

import { parseSelectorSource, evalSelector } from "@archlang/engine";
const { selector, errors } = parseSelectorSource(`@@team:"Payments"`);
if (errors.length) throw new Error(errors[0]!.message);
const result = evalSelector(selector, ctx);
// result.sort === "node" → result.nodes is a Set<string> of matched element keys
// result.sort === "edge" → result.edges is a Set<DependencyEdge>

Вычисление политик. Правила forbid/require декларации policy компилируются в Finding. Соберите политики из распарсенных файлов (для политик внутри тела модуля нужно проштамповать область поддерева через collectScopedPolicies), затем вычислите:

import { collectScopedPolicies, evalPolicies, activeFindings, type PolicyDecl } from "@archlang/engine";
const topLevel: PolicyDecl[] = [];
for (const file of parsedFiles) {
for (const d of file.declarations) if (d.kind === "policy") topLevel.push(d.decl);
}
const policies = collectScopedPolicies(cascaded, topLevel).policies;
const findings = evalPolicies(policies, ctx, {
onError: (policy, err) => console.warn(`policy ${policy.name} skipped: ${err}`),
});
const active = activeFindings(findings); // waived findings filtered out

Каждая Finding несёт policy, ruleKind ("forbid" | "require"), sort ("node" | "edge"), anchorKey, severity и исходные span’ы как для правила, так и для места нарушения — достаточно, чтобы отрисовать аннотацию CI или диагностику редактора. Это ровно тот конвейер, который выполняет archlang policy-check; полную эталонную реализацию, включая --strict и обработку истечения waiver’ов, смотрите в packages/cli/src/commands/policy-check.ts.

Гейты изменений. Гейты when (when <subject> added { require review … }) срабатывают на изменении база→head, а не на одной модели, так что им нужны обе версии:

import { evalChangeGates } from "@archlang/engine";
const fired = evalChangeGates(policies, baseModel, headModel);
// fired[].policy, .verb ("added"|"removed"|"changed"), .matched (element keys), .reviews

evalPoliciesAtGategate-waivers.ts) — двухмодельный собрат evalPolicies: он вычисляет находки state-правил на стороне head, одновременно разрешая действенность waiver’ов относительно и базы, и head, — это то правило, которое accept-гейт Studio выполняет над предложением (proposal) перед слиянием.

Проекции (views). planView(view, ctx, viewsByName?) проецирует ViewDeclV2 (декларацию show/hide/group/style) в ViewProjectionPlan — набор видимых узлов, групп и переопределений стиля, которые потребляет рендерер доски (board). resolveViewInstance вливает привязки экземпляра view <Parent> <Name> { … } в его родителя перед проекцией; resolveKnobBindings разрешает параметры knob проекции. Эти три функции — то, чем LSP и конвейер проекций доски превращают декларацию view в пиксели; если нужен просто набор совпавших узлов, а не полный план рендера, обращайтесь напрямую к evalSelector.

Окно терминала
npm install @archlang/lsp

Пакет LSP служит трём целям:

  1. Загрузка пакетов из источника, похожего на файловую систему.
  2. Запуск языкового сервера.
  3. Каноническое форматирование текста исходника.
import { loadPackage, type LoadedPackage } from "@archlang/lsp";
import { ioNode } from "@archlang/lsp/node";
import { pathToFileURL } from "node:url";
const pkg: LoadedPackage = await loadPackage(ioNode(), pathToFileURL("/path/to/package").toString());

loadPackage(io, rootUri, options?) обходит корень пакета, парсит каждый .arch файл, рекурсивно загружает зависимости, разрешает пакет и возвращает LoadedPackage. rootUri — это URI (file://… в Node — используйте pathToFileURL), а не голый путь файловой системы. Разрешённая модель и её диагностики уже есть в результате:

import { loadPackage, type LoadedPackage } from "@archlang/lsp";
import { ioNode } from "@archlang/lsp/node";
import { applyCascade, validate } from "@archlang/engine";
import { pathToFileURL } from "node:url";
const loaded: LoadedPackage = await loadPackage(ioNode(), pathToFileURL("/path/to/package").toString());
const cascaded = applyCascade(loaded.resolved.model);
const validation = validate(cascaded);
const allDiagnostics = [...loaded.resolved.diagnostics, ...validation.diagnostics];

Если вместо этого вам нужны компоненты до разрешения — например, чтобы самостоятельно вызвать resolvePackage, как это делает инкрементальный компилятор — discoverPackage(io, rootUri, options?) возвращает DiscoveredPackage с .input и .deps (PackageMap) вместо уже разрешённой модели.

Интерфейс LspIO абстрагирует доступ к файловой системе. ioNode() (из @archlang/lsp/node) строит реализацию на базе Node fs; браузерный хост предоставляет свою (VFS в памяти), удовлетворяющую тому же интерфейсу.

import { formatSource } from "@archlang/lsp";
const result = formatSource(originalSource);
// result.text — formatted source
// result.changed — boolean: did anything change?

formatSource(input: string): FormatSourceResult — чистая функция из текста исходника в канонически отформатированный текст. Полезна для pre-commit хуков, форматирования при сохранении в кастомных редакторах и инструментов генерации, которые выдают .arch исходник.

// Node-side, stdio (what editor extensions use)
import { startNodeServer } from "@archlang/lsp/node";
startNodeServer();
// Browser-side, web worker (what the hosted demo uses)
import { startBrowserServer } from "@archlang/lsp/browser";
startBrowserServer(virtualFileSystem);

Вы вызываете это напрямую только если строите новую интеграцию редактора. Существующие расширения сами обрабатывают разводку. Поверхность протокола — стандартный LSP: initialize, textDocument/completion, textDocument/hover и т.д.

Окно терминала
npm install @archlang/render
import { rasterizeSvg } from "@archlang/render";
const png: Buffer = await rasterizeSvg(svgText);

rasterizeSvg(svg: string, opts?: RasterizeOptions): Promise<Buffer> превращает строку SVG в буфер PNG через resvg, со встроенными шрифтами Inter, так что текст рендерится идентично без зависимости от системных шрифтов — тот же путь, которым пользуются команда CLI archlang render и headless-рендеры view/diff сервера MCP. @archlang/render сам не парсит и не раскладывает исходник .arch; это растеризующая половина конвейера, который начинается с toSVG из @archlang/scene (чистая пиксельная геометрия + композиция сцены поверх разрешённой модели/графа движка) и заканчивается здесь.

Разобранный пример: CI-бот, комментирующий архитектурные дельты PR

Заголовок раздела «Разобранный пример: CI-бот, комментирующий архитектурные дельты PR»
import { loadPackage } from "@archlang/lsp";
import { ioNode } from "@archlang/lsp/node";
import { applyCascade, diffModels } from "@archlang/engine";
import { pathToFileURL } from "node:url";
async function describeDelta(beforePath: string, afterPath: string): Promise<string> {
const io = ioNode();
const beforeLoaded = await loadPackage(io, pathToFileURL(beforePath).toString());
const afterLoaded = await loadPackage(io, pathToFileURL(afterPath).toString());
const before = applyCascade(beforeLoaded.resolved.model);
const after = applyCascade(afterLoaded.resolved.model);
const delta = diffModels(before, after);
const lines: string[] = [];
for (const [id, m] of delta.modules) {
if (m.status === "renamed" && m.nameChange) {
lines.push(`- **Renamed:** \`${m.nameChange.from}\`\`${m.nameChange.to}\``);
} else if (m.status === "added") {
lines.push(`- **Added module:** \`${m.name}\` (\`${id}\`)`);
} else if (m.status === "removed") {
lines.push(`- **Removed module:** \`${m.name}\` (\`${id}\`)`);
}
}
return lines.join("\n");
}

Примерно 30 строк связующего кода. Сводка архитектурной дельты в PR теперь автоматическая.

Все опубликованные пакеты следуют семантическому версионированию. Внутри линии 0.x поверхность протокола LSP и коды диагностик считаются полустабильными: добавления — минорные; переименования или удаления — минорные с явными отметками в журнале изменений.

Форма AST и форма разрешённой модели считаются нестабильными внутри 0.x. Авторам инструментов, строящим на их основе, стоит зафиксировать конкретную версию и перетестировать при каждом апгрейде. Формы стабилизируются в 1.0.

  • Headless-рендеринг — задача @archlang/render, и лишь половина конвейера. Он растеризует SVG → PNG; он не парсит .arch и не раскладывает диаграмму. Сам SVG приходит из toSVG пакета @archlang/scene поверх разрешённой модели и графа движка. Библиотечные API этой главы дают вам модель и граф; превращение их в пиксели — отдельная забота, надстроенная сверху.
  • Нет интеграции с git. loadPackage читает через интерфейс LspIO. Инструменты, осведомлённые о git (коммитят снимки, считают дельты веток), живут в CLI и в пользовательском коде.
  • Нет HTTP API. Ни один из этих пакетов не открывает порт. Если нужна сервисная поверхность — оберните библиотеку в свой сервер.
  • @archlang/engine — исходник → AST + лексер, разрешённая модель + валидация + граф + дифф + рендер описаний + API вычисления селекторов/политик/проекций v0.10.
  • @archlang/lsp — загрузчик пакетов (loadPackage, discoverPackage), языковой сервер (startNodeServer, startBrowserServer), форматтер исходника (formatSource).
  • @archlang/render — headless-растеризация SVG → PNG (rasterizeSvg).
  • loadPackage живёт в @archlang/lsp, потому что ему нужен LspIO (файловая система); его результат уже несёт разрешённую модель — выполните на ней applyCascade перед валидацией, построением графа или диффом.
  • evalSelector/evalPolicies/evalChangeGates/planView — чистые вычислители за archlang policy-check и конвейером проекций доски; обращайтесь к ним напрямую, когда нужны структурированные находки или набор совпавших узлов вместо кода выхода CLI.
  • Формы AST и разрешённой модели нестабильны внутри 0.x; LSP и коды диагностик полустабильны.

Глава 25: SaaS-бэкенд → — открывает часть VI, шесть разобранных проектов с реалистичными системами, смоделированными в ArchLang.