24. Library APIs
The CLI, editor extensions, and viewer are all built on three published packages: @archlang/parser, @archlang/core, and @archlang/lsp. If you’re building something new — a custom validator, a CI bot, a documentation generator, a non-JS-platform integration — you’ll consume one or more of these directly.
This chapter is for tool authors. It assumes JavaScript / TypeScript and Node.js familiarity. If you only consume .arch files through the editor and CLI, skip to Chapter 25.
The three packages
| Package | What it gives you | Use when |
|---|---|---|
@archlang/parser | Source text → AST + lexer | You only need syntax, not resolution |
@archlang/core | AST → resolved model, validation, graph, diff, description rendering | You want the full canonical model |
@archlang/lsp | Package loader + language server + format helper | You need filesystem loading, an editor server, or canonical formatting |
core depends on parser. lsp depends on core. Pick the highest level you need.
The package boundary is worth knowing: loadPackage lives in @archlang/lsp, not in core. It handles filesystem walking and dependency resolution, which only matter when you have a workspace to read from. Pure analysis on already-parsed sources uses core alone.
@archlang/parser
npm install @archlang/parserimport { parse } from "@archlang/parser";
const source = ` service Payments { team: Payments command Authorize }`;
const result = parse(source, "/main.arch");// result.file — ParsedFile (declarations, uses, manifest if any)// result.diagnostics — Diagnostic[] (parse errors, each with span + message + code)parse(source: string, file?: string) is a pure function from source text to a parsed-file AST plus diagnostics. The optional file parameter is recorded on each AST span so downstream tooling can locate errors. No name resolution, no validation beyond syntactic well-formedness, no cross-file work.
Typical uses:
- Syntax highlighting in a non-LSP environment.
- Find-by-string tools walking source for declarations matching a pattern.
- Migration scripts that rewrite source text — parse to find positions, then patch the source directly.
The AST types are exported from the package: ModuleDecl, InterfaceDecl, FacetDecl, ProcessDecl, ViewDecl, TypeDecl, SubprocessDecl, and the supporting span / value / declaration types.
@archlang/core
npm install @archlang/corecore consumes parsed files, resolves the model, runs the validator, computes the dependency graph, and produces diffs. This is what every higher-level tool builds on.
Resolving a package
import { resolvePackage, type PackageInput, type PackageMap } from "@archlang/core";
const result = resolvePackage(input, deps);// result.model — ResolvedModel (canonical resolved view)// result.diagnostics — Diagnostic[] (resolution + validation diagnostics)resolvePackage(pkg: PackageInput, deps: PackageMap) takes a parsed package (its manifest + parsed files) and a map of its dependencies. It applies type-template stamping (Chapter 15), structural cascade (Chapter 18), and emits a ResolvedModel.
A ResolvedModel exposes arrays of resolved modules, facets, interfaces, processes, and views:
import type { ResolvedModel, ResolvedModule } from "@archlang/core";
function totalInterfaces(model: ResolvedModel): number { return model.modules.reduce((sum, m) => sum + m.interfaces.length, 0);}Validation
import { validate } from "@archlang/core";
const result = validate(model);// result.diagnostics — array of validator diagnosticsvalidate produces every diagnostic the validator can emit: missing required blanks, invalid process step references, label invariant violations, cross-package reference failures. Codes are stable; severities can be overridden by a project’s config.
Building the dependency graph
import { buildGraph } from "@archlang/core";
const graph = buildGraph(model);// graph.edges — DependencyEdge[] (one per derived call)// graph.modulesById — Map<StableId, ResolvedModule>// graph.adjacency — Map<StableId, StableId[]>Every edge in the graph comes from a process step (or a subscribes: wiring). The graph is what views, blast-radius analysis, and impact reports run on.
Useful helpers in the same module:
import { getDependents, getDependencies, computeBlastRadius } from "@archlang/core";
const dependents = getDependents(graph, "#pay001");const dependencies = getDependencies(graph, "#pay001");const blastRadius = computeBlastRadius(graph, "#pay001");Computing diffs
import { diffModels, type ChangeSet, type ModuleDiff } from "@archlang/core";
const delta: ChangeSet = diffModels(beforeModel, afterModel);// delta.modules — ReadonlyMap<ModuleId, ModuleDiff>// delta.processes — ReadonlyMap<ProcessId, ProcessDiff>// delta.views — ReadonlyMap<ViewId, ViewDiff>Each ModuleDiff carries a status (one of added | removed | modified | renamed | unchanged) plus the per-field, per-facet, per-interface changes. Iterate:
for (const [id, mDiff] of delta.modules) { if (mDiff.status === "renamed") { console.log(`Renamed: ${mDiff.fromName} → ${mDiff.toName} (${id})`); } else if (mDiff.status === "added") { console.log(`Added: ${mDiff.toName} (${id})`); } else if (mDiff.status === "removed") { console.log(`Removed: ${mDiff.fromName} (${id})`); }}This is the engine the viewer’s diff mode uses (Chapter 14). Pipelines that need machine-readable architecture deltas consume diffModels directly.
Rendering descriptions
import { renderDescription, makeDescriptionContext } from "@archlang/core";
const ctx = makeDescriptionContext(model, { node: someResolvedModule });const html = renderDescription(rawDescriptionText, ctx);renderDescription is the same renderer the LSP uses for hover and the viewer uses for tooltips. makeDescriptionContext builds the context object (label lookup, [[ref]] resolver). Use them when building documentation pages that need parity with editor displays.
@archlang/lsp
npm install @archlang/lspThe LSP package serves three purposes:
- Loading packages from a filesystem-like source.
- Running the language server.
- Formatting source text canonically.
Loading a package
import { loadPackage, type LoadedPackage } from "@archlang/lsp";
const pkg: LoadedPackage = await loadPackage(io, "/path/to/package");loadPackage walks the package root, parses every .arch file, recursively loads dependencies, and returns a LoadedPackage (a PackageInput plus a PackageMap of resolved dependencies). Pair it with resolvePackage from core:
import { loadPackage } from "@archlang/lsp";import { resolvePackage, validate } from "@archlang/core";import { NodeIO } from "@archlang/lsp/node";
const io = new NodeIO();const loaded = await loadPackage(io, "/path/to/package");const resolved = resolvePackage(loaded.input, loaded.deps);const validation = validate(resolved.model);The LspIO interface abstracts filesystem access. Node and browser implementations satisfy it; pass whichever fits your runtime.
Formatting source
import { formatSource } from "@archlang/lsp";
const result = formatSource(originalSource);// result.text — formatted source// result.changed — boolean: did anything change?formatSource(input: string): FormatSourceResult is a pure function from source text to canonically-formatted source text. Useful for pre-commit hooks, format-on-save in custom editors, and code-generation tools that produce .arch source.
Running the language server
// 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);You only invoke these directly if you’re building a new editor integration. Existing extensions handle the wiring for you. The protocol surface is standard LSP — initialize, textDocument/completion, textDocument/hover, etc.
A worked example: a CI bot that comments on PR architecture deltas
import { loadPackage } from "@archlang/lsp";import { NodeIO } from "@archlang/lsp/node";import { resolvePackage, diffModels } from "@archlang/core";
async function describeDelta(beforePath: string, afterPath: string): Promise<string> { const io = new NodeIO();
const beforeLoaded = await loadPackage(io, beforePath); const afterLoaded = await loadPackage(io, afterPath);
const before = resolvePackage(beforeLoaded.input, beforeLoaded.deps).model; const after = resolvePackage(afterLoaded.input, afterLoaded.deps).model;
const delta = diffModels(before, after);
const lines: string[] = []; for (const [id, m] of delta.modules) { if (m.status === "renamed") { lines.push(`- **Renamed:** \`${m.fromName}\` → \`${m.toName}\``); } else if (m.status === "added") { lines.push(`- **Added module:** \`${m.toName}\` (\`${id}\`)`); } else if (m.status === "removed") { lines.push(`- **Removed module:** \`${m.fromName}\` (\`${id}\`)`); } } return lines.join("\n");}About 30 lines of glue. The architecture-delta summary in PRs is now automatic.
Versioning
All three packages follow semantic versioning. Within a 0.x line, the LSP protocol surface and diagnostic codes are considered semi-stable: additions are minor; renames or removals are minor with explicit changelog notes.
The AST shape and the resolved-model shape are considered unstable within 0.x. Tool authors building against them should pin a specific version and re-test on each upgrade. The shapes will stabilize at 1.0.
What’s not in the libraries
- No renderer. Diagram rendering happens in
@archlang/web. The library APIs give you the model and the graph; rendering is a separate concern. - No git integration.
loadPackagereads via theLspIOinterface. Git-aware tooling (committing snapshots, computing branch deltas) lives in the CLI and in user code. - No HTTP API. None of the three packages opens a port. If you want a service surface, wrap the library in your own server.
Summary
@archlang/parser— source → AST + lexer.@archlang/core— AST → resolved model + validation + graph + diff + description rendering.@archlang/lsp— package loader (loadPackage), language server (startNodeServer,startBrowserServer), source formatter (formatSource).loadPackagelives in@archlang/lspbecause it needsLspIO(filesystem); pair it withresolvePackagefromcorefor analysis.- AST and resolved-model shapes are unstable within
0.x; LSP and diagnostic codes are semi-stable.
What’s next
Chapter 25: A SaaS Backend → — opens Part VI, six worked designs showing realistic systems modeled in Archlang.