Skip to content

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

PackageWhat it gives youUse when
@archlang/parserSource text → AST + lexerYou only need syntax, not resolution
@archlang/coreAST → resolved model, validation, graph, diff, description renderingYou want the full canonical model
@archlang/lspPackage loader + language server + format helperYou 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

Terminal window
npm install @archlang/parser
import { 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

Terminal window
npm install @archlang/core

core 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 diagnostics

validate 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

Terminal window
npm install @archlang/lsp

The LSP package serves three purposes:

  1. Loading packages from a filesystem-like source.
  2. Running the language server.
  3. 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. loadPackage reads via the LspIO interface. 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).
  • loadPackage lives in @archlang/lsp because it needs LspIO (filesystem); pair it with resolvePackage from core for 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.