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/render | Headless-растеризация SVG → PNG (resvg + встроенные шрифты) | Нужны изображения диаграмм вне браузера — артефакты CI, сборка документации, инструменты MCP |
lsp зависит от engine. render независим — он растеризует SVG-текст, произведённый чем-то другим (CLI, toSVG из @archlang/scene, или Viewer’ом); сам он никогда не парсит .arch. Выбирайте пакет, подходящий под задачу.
Стоит знать о границе пакетов: loadPackage живёт в @archlang/lsp, а не в engine. Он обрабатывает обход файловой системы и разрешение зависимостей, что важно только когда есть рабочее пространство для чтения. Чистый анализ уже распарсенных исходников использует только engine.
@archlang/engine
Заголовок раздела «@archlang/engine»npm install @archlang/engineengine — это весь язык целиком: синтаксис (лексер + парсер) и семантика (резолвер, каскад, граф, дифф, рендер описаний, вычисление 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 diagnosticsvalidate выдаёт каждую диагностику, которую может породить валидатор: пропущенные обязательные поля, неверные ссылки в шагах процесса, нарушения инвариантов аспектов, провалы межпакетных ссылок. Коды стабильны; уровни важности могут быть переопределены конфигом проекта.
Построение графа зависимостей
Заголовок раздела «Построение графа зависимостей»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 + permanentconst 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), .reviewsevalPoliciesAtGate (в gate-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.
@archlang/lsp
Заголовок раздела «@archlang/lsp»npm install @archlang/lspПакет LSP служит трём целям:
- Загрузка пакетов из источника, похожего на файловую систему.
- Запуск языкового сервера.
- Каноническое форматирование текста исходника.
Загрузка пакета
Заголовок раздела «Загрузка пакета»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 и т.д.
@archlang/render
Заголовок раздела «@archlang/render»npm install @archlang/renderimport { 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.