14  @panproto/core: The TypeScript SDK

The @panproto/core npm package wraps the WASM module in a fluent, type-safe API with automatic resource management. Most developers interact with panproto through this SDK.

Tip

For a quick-reference listing of all public APIs with parameter types, see tutorial appendix B.

14.1 Architecture overview

The SDK is organized into six core classes, each with a focused responsibility:

classDiagram
    class Panproto {
        +init() Panproto$
        +protocol(name) Protocol
        +defineProtocol(spec) Protocol
        +migration(src, tgt) MigrationBuilder
        +checkExistence(src, tgt, builder) ExistenceReport
        +compose(m1, m2) CompiledMigration
        +diff(old, new) DiffReport
        +Symbol.dispose()
    }

    class Protocol {
        +name: string
        +spec: ProtocolSpec
        +schema() SchemaBuilder
        +Symbol.dispose()
    }

    class SchemaBuilder {
        +vertex(id, kind, opts?) SchemaBuilder
        +edge(src, tgt, kind, opts?) SchemaBuilder
        +hyperEdge(id, kind, sig, parent) SchemaBuilder
        +constraint(vertex, sort, value) SchemaBuilder
        +required(vertex, edges) SchemaBuilder
        +build() BuiltSchema
    }

    class BuiltSchema {
        +data: SchemaData
        +protocol: string
        +vertices: Record
        +edges: Edge[]
        +Symbol.dispose()
    }

    class MigrationBuilder {
        +map(src, tgt) MigrationBuilder
        +mapEdge(src, tgt) MigrationBuilder
        +resolve(src, tgt, edge) MigrationBuilder
        +compile() CompiledMigration
    }

    class CompiledMigration {
        +spec: MigrationSpec
        +lift(record) LiftResult
        +get(record) GetResult
        +put(view, complement) LiftResult
        +Symbol.dispose()
    }

    Panproto --> Protocol : protocol()
    Protocol --> SchemaBuilder : schema()
    SchemaBuilder --> BuiltSchema : build()
    Panproto --> MigrationBuilder : migration()
    MigrationBuilder --> CompiledMigration : compile()

14.2 The panproto class

Panproto is the main entry point. It loads the WASM module and provides the top-level API:

export class Panproto implements Disposable {
  readonly #wasm: WasmModule;
  readonly #protocols: Map<string, Protocol>;

  private constructor(wasm: WasmModule) {
    this.#wasm = wasm;
    this.#protocols = new Map();
  }

  /** The WASM module reference. Internal use only. */
  get _wasm(): WasmModule {
    return this.#wasm;
  }

  /**
   * Initialize the panproto SDK by loading the WASM module.
   *
   * @param input - URL to the wasm-bindgen glue module, or a pre-imported
   *                glue module object (for bundler environments like Vite).
   *                Defaults to the bundled glue module.
   * @returns An initialized Panproto instance
   * @throws {@link import('./types.js').WasmError} if WASM loading fails

Key methods:

  • init(wasmUrl?): Async factory. Loads the WASM binary (bundled by default) and returns a ready instance.
  • protocol(name): Get or auto-register a built-in protocol (atproto, sql, protobuf, graphql, json-schema). Custom protocols must be registered first with defineProtocol.
  • defineProtocol(spec): Register a custom protocol specification.
  • migration(src, tgt): Start building a migration between two schemas.
  • checkExistence(src, tgt, builder): Validate a migration against protocol-derived existence conditions.
  • compose(m1, m2): Compose two compiled migrations into one.
  • diff(old, new): Compute a structural diff between two schemas.

Panproto implements Disposable. When disposed, it releases all cached protocol handles.

14.3 Protocol

A Protocol holds a WASM-side handle to the registered protocol specification and provides the schema() factory:

export class Protocol implements Disposable {
  readonly #handle: WasmHandle;
  readonly #spec: ProtocolSpec;
  readonly #wasm: WasmModule;

  constructor(handle: WasmHandle, spec: ProtocolSpec, wasm: WasmModule) {
    this.#handle = handle;
    this.#spec = spec;
    this.#wasm = wasm;
  }

  /** The protocol name. */
  get name(): string {
    return this.#spec.name;
  }

  /** The full protocol specification. */
  get spec(): ProtocolSpec {
    return this.#spec;
  }

  /** The edge rules for this protocol. */
  get edgeRules(): readonly EdgeRule[] {
    return this.#spec.edgeRules;
  }

  /** The constraint sorts for this protocol. */
  get constraintSorts(): readonly string[] {
    return this.#spec.constraintSorts;
  }

  /** The object kinds for this protocol. */
  get objectKinds(): readonly string[] {
    return this.#spec.objKinds;
  }

  /** The WASM handle. Internal use only. */
  get _handle(): WasmHandle {
    return this.#handle;
  }

Five built-in protocol specs are provided: ATPROTO_SPEC, SQL_SPEC, PROTOBUF_SPEC, GRAPHQL_SPEC, and JSON_SCHEMA_SPEC. They’re auto-registered on first access via panproto.protocol('atproto').

14.4 Schemabuilder and builtschema

SchemaBuilder is an immutable fluent builder. Each method returns a new builder instance, leaving the original unchanged. This makes it safe to branch schema definitions:

 *   .build();
 * ```
 */
export class SchemaBuilder {
  readonly #protocolName: string;
  readonly #protocolHandle: WasmHandle;
  readonly #wasm: WasmModule;
  readonly #ops: readonly SchemaOp[];
  readonly #vertices: ReadonlyMap<string, Vertex>;
  readonly #edges: readonly Edge[];
  readonly #hyperEdges: ReadonlyMap<string, HyperEdge>;
  readonly #constraints: ReadonlyMap<string, readonly Constraint[]>;
  readonly #required: ReadonlyMap<string, readonly Edge[]>;

  constructor(
    protocolName: string,
    protocolHandle: WasmHandle,
    wasm: WasmModule,
    ops: readonly SchemaOp[] = [],
    vertices: ReadonlyMap<string, Vertex> = new Map(),
    edges: readonly Edge[] = [],
    hyperEdges: ReadonlyMap<string, HyperEdge> = new Map(),
    constraints: ReadonlyMap<string, readonly Constraint[]> = new Map(),
    required: ReadonlyMap<string, readonly Edge[]> = new Map(),
  ) {
    this.#protocolName = protocolName;
    this.#protocolHandle = protocolHandle;
    this.#wasm = wasm;
    this.#ops = ops;
    this.#vertices = vertices;
    this.#edges = edges;
    this.#hyperEdges = hyperEdges;

The builder accumulates SchemaOp objects (matching the Rust BuildOp tagged enum). On .build(), it packs them as MessagePack, sends them to WASM, and wraps the returned handle:

   * @returns The validated, built schema
   * @throws {@link SchemaValidationError} if the schema is invalid
   */
  build(): BuiltSchema {
    const opsBytes = packSchemaOps([...this.#ops]);
    const rawHandle = this.#wasm.exports.build_schema(
      this.#protocolHandle.id,
      opsBytes,
    );

    const handle = createHandle(rawHandle, this.#wasm);

    const data: SchemaData = {
      protocol: this.#protocolName,
      vertices: Object.fromEntries(this.#vertices),
      edges: [...this.#edges],
      hyperEdges: Object.fromEntries(this.#hyperEdges),
      constraints: Object.fromEntries(
        Array.from(this.#constraints.entries()).map(([k, v]) => [k, [...v]]),
      ),
      required: Object.fromEntries(
        Array.from(this.#required.entries()).map(([k, v]) => [k, [...v]]),
      ),
      variants: {},

BuiltSchema is the validated result. It holds both a WASM handle (for passing to migration functions) and a local SchemaData snapshot (for introspection without crossing the boundary):

    return new BuiltSchema(handle, data, this.#wasm);
  }
}

/**
 * A validated, built schema with a WASM-side handle.
 *
 * Implements `Disposable` for automatic cleanup of the WASM resource.
 */
export class BuiltSchema implements Disposable {
  readonly #handle: WasmHandle;
  readonly #data: SchemaData;
  readonly #wasm: WasmModule;

  constructor(handle: WasmHandle, data: SchemaData, wasm: WasmModule) {
    this.#handle = handle;
    this.#data = data;
    this.#wasm = wasm;
  }

  /** The WASM handle for this schema. Internal use only. */
  get _handle(): WasmHandle {
    return this.#handle;
  }

  /** The WASM module reference. Internal use only. */
  get _wasm(): WasmModule {
    return this.#wasm;
  }

  /** The schema data (vertices, edges, constraints, etc.). */
  get data(): SchemaData {
    return this.#data;
  }

  /** The protocol name this schema belongs to. */
  get protocol(): string {
    return this.#data.protocol;
  }

  /** All vertices in the schema. */
  get vertices(): Readonly<Record<string, Vertex>> {
    return this.#data.vertices;
  }

  /** All edges in the schema. */

14.5 Migrationbuilder and compiledmigration

MigrationBuilder follows the same immutable fluent pattern. It accumulates vertex mappings, edge mappings, and resolvers:

export class MigrationBuilder {
  readonly #src: BuiltSchema;
  readonly #tgt: BuiltSchema;
  readonly #wasm: WasmModule;
  readonly #vertexMap: ReadonlyMap<string, string>;
  readonly #edgeMap: readonly [Edge, Edge][];
  readonly #resolvers: readonly [[string, string], Edge][];

  constructor(
    src: BuiltSchema,
    tgt: BuiltSchema,
    wasm: WasmModule,
    vertexMap: ReadonlyMap<string, string> = new Map(),
    edgeMap: readonly [Edge, Edge][] = [],
    resolvers: readonly [[string, string], Edge][] = [],
  ) {
    this.#src = src;
    this.#tgt = tgt;
    this.#wasm = wasm;
    this.#vertexMap = vertexMap;
    this.#edgeMap = edgeMap;
    this.#resolvers = resolvers;
  }

  /**
   * Map a source vertex to a target vertex.
   *
   * @param srcVertex - Vertex id in the source schema
   * @param tgtVertex - Vertex id in the target schema
   * @returns A new builder with the mapping added
   */
  map(srcVertex: string, tgtVertex: string): MigrationBuilder {
    const newMap = new Map(this.#vertexMap);
    newMap.set(srcVertex, tgtVertex);

    return new MigrationBuilder(
      this.#src,
      this.#tgt,
      this.#wasm,
      newMap,
      this.#edgeMap,
      this.#resolvers,
    );
  }

The compile() method packs the mapping and sends it to the compile_migration WASM entry point. The result is a CompiledMigration with three data-path methods:


  /**
   * Compile the migration for fast per-record application.
   *
   * Sends the migration specification to WASM for compilation.
   * The resulting `CompiledMigration` can be used to transform records.
   *
   * @returns A compiled migration ready for record transformation
   * @throws {@link MigrationError} if compilation fails
   */
  compile(): CompiledMigration {
    const edgeMap = new Map(
      this.#edgeMap.map(([src, tgt]) => [
        { src: src.src, tgt: src.tgt, kind: src.kind, name: src.name ?? null },
        { src: tgt.src, tgt: tgt.tgt, kind: tgt.kind, name: tgt.name ?? null },
      ] as const),
    );
    const resolver = new Map(
      this.#resolvers.map(([[s, t], e]) => [
        [s, t] as const,
        { src: e.src, tgt: e.tgt, kind: e.kind, name: e.name ?? null },
      ] as const),
    );
    const mapping = packMigrationMapping({
      vertex_map: Object.fromEntries(this.#vertexMap),
      edge_map: edgeMap,
      hyper_edge_map: {},
      label_map: new Map(),
      resolver,
    });

    try {
      const rawHandle = this.#wasm.exports.compile_migration(
        this.#src._handle.id,
        this.#tgt._handle.id,
        mapping,
      );

      const handle = createHandle(rawHandle, this.#wasm);
      return new CompiledMigration(handle, this.#wasm, this.toSpec());
    } catch (error) {
      throw new MigrationError(
        `Failed to compile migration: ${error instanceof Error ? error.message : String(error)}`,
        { cause: error },
      );
    }
  }
}
  • lift(record): Forward-only transformation. The hot path: data goes through WASM as MessagePack bytes with no intermediate JS-heap allocation.
  • get(record): Bidirectional get: extract a projected view and an opaque complement.
  • put(view, complement): Bidirectional put: restore a full record from a (possibly modified) view and the complement from get.
Note

The complement from get() is an opaque Uint8Array. It captures the data discarded by the forward projection, enabling lossless round-tripping. Treat it as a black box; its internal format is a MessagePack-encoded Rust Complement struct.

14.6 Wasmhandle and Resource safety

Every WASM-side resource is wrapped in a WasmHandle, which implements Disposable for use with the using declaration:

      parse_expr: glue.parse_expr,
      eval_func_expr: glue.eval_func_expr,
      execute_query: glue.execute_query,
      // Phase 12: Fiber, hom, and graph
      fiber_at: glue.fiber_at,
      fiber_decomposition_wasm: glue.fiber_decomposition_wasm,
      poly_hom: glue.poly_hom,
      preferred_conversion_path: glue.preferred_conversion_path,
      conversion_distance: glue.conversion_distance,
    };

    const memory: WebAssembly.Memory = initOutput.memory;

    if (!memory) {
      throw new WasmError('WASM module missing memory export');
    }

    return { exports, memory };
  } catch (error) {
    if (error instanceof WasmError) throw error;
    throw new WasmError(
      `Failed to load WASM module: ${error instanceof Error ? error.message : String(error)}`,
      { cause: error },
    );
  }
}

// ---------------------------------------------------------------------------
// Handle registry — prevents resource leaks
// ---------------------------------------------------------------------------

/** Weak reference registry for leaked handle detection. */
const leakedHandleRegistry = new FinalizationRegistry<CleanupInfo>((info) => {
  // Safety net: if a disposable wrapper is GC'd without being disposed,
  // free the underlying WASM handle.
  try {
    info.freeHandle(info.handle);
  } catch {
    // WASM module may already be torn down; swallow.

Two layers of safety prevent handle leaks:

  1. Symbol.dispose: Explicit cleanup. Called automatically by using or manually by the consumer.
  2. FinalizationRegistry: Safety net. If a WasmHandle is garbage-collected without being disposed, the registry calls free_handle to release the WASM resource.
Important

The FinalizationRegistry is a last resort, not a primary mechanism. GC timing is non-deterministic, so relying on it can cause resource exhaustion under load. Always prefer explicit disposal via using or Symbol.dispose().

TipForgetting to Dispose a Handle

The FinalizationRegistry will eventually clean it up, but “eventually” is unpredictable. Under load, leaked handles accumulate until GC runs, which can exhaust WASM memory. Always use using or explicit [Symbol.dispose]() calls.

14.7 Messagepack encoding helpers

The msgpack.ts module provides four encoding helpers that wrap @msgpack/msgpack:

export function packToWasm(value: unknown): Uint8Array {
  return encode(value);
}

/**
 * Decode MessagePack bytes received from WASM.
 *
 * @typeParam T - The expected decoded type
 * @param bytes - MessagePack-encoded bytes from WASM
 * @returns The decoded value
 */
export function unpackFromWasm<T = unknown>(bytes: Uint8Array): T {
  return decode(bytes) as T;
}

/**
 * Encode a schema operations list for the `build_schema` entry point.
 *
 * @param ops - Array of builder operations
 * @returns MessagePack-encoded bytes
 */
export function packSchemaOps(ops: readonly SchemaOp[]): Uint8Array {
  return encode(ops);
}

/**
 * Encode a migration mapping for WASM entry points.
 *
 * @param mapping - The migration mapping object
 * @returns MessagePack-encoded bytes
 */
export function packMigrationMapping(mapping: MigrationMapping): Uint8Array {
  // The Rust Migration struct uses `map_as_vec` for fields with non-string keys,
  // which deserializes from a sequence of [key, value] pairs, not a map.
  • packToWasm(value): Encode any JS value to MessagePack bytes for a WASM entry point.
  • unpackFromWasm<T>(bytes): Decode MessagePack bytes from WASM into a typed JS value.
  • packSchemaOps(ops): Encode a SchemaOp[] for the build_schema entry point.
  • packMigrationMapping(mapping): Encode a MigrationMapping for compile_migration or check_existence.

The SchemaOp interface uses serde’s internally-tagged format: the op field acts as the discriminant, and all variant fields sit at the same level.

14.8 Lens API

The lens.ts module provides three handle classes for bidirectional schema transformations: ProtolensChainHandle for schema-independent lens families, LensHandle for concrete lenses, and SymmetricLensHandle for symmetric bidirectional sync.

ProtolensChainHandle wraps a WASM-side protolens chain and can be instantiated against a concrete schema to produce a LensHandle:

export class ProtolensChainHandle implements Disposable {
  readonly #handle: WasmHandle;
  readonly #wasm: WasmModule;

  constructor(handle: WasmHandle, wasm: WasmModule) {
    this.#handle = handle;
    this.#wasm = wasm;
  }

  /** The underlying WASM handle. Internal use only. */
  get _handle(): WasmHandle {
    return this.#handle;
  }

  /**
   * Auto-generate a protolens chain between two schemas.
   *
   * @param schema1 - The source schema
   * @param schema2 - The target schema
   * @param wasm - The WASM module
   * @returns A ProtolensChainHandle wrapping the generated chain
   * @throws {@link WasmError} if the WASM call fails
   */
  static autoGenerate(schema1: BuiltSchema, schema2: BuiltSchema, wasm: WasmModule): ProtolensChainHandle {
    try {
      const rawHandle = wasm.exports.auto_generate_protolens(schema1._handle.id, schema2._handle.id);
      return new ProtolensChainHandle(createHandle(rawHandle, wasm), wasm);
    } catch (error) {
      throw new WasmError(
        `auto_generate_protolens failed: ${error instanceof Error ? error.message : String(error)}`,
        { cause: error },
      );
    }
  }

  /**
   * Instantiate this protolens chain against a concrete schema.
   *
   * @param schema - The schema to instantiate against
   * @returns A LensHandle for the instantiated lens
   * @throws {@link WasmError} if the WASM call fails
   */
  instantiate(schema: BuiltSchema): LensHandle {
    try {
      const rawHandle = this.#wasm.exports.instantiate_protolens(this.#handle.id, schema._handle.id);
      return new LensHandle(createHandle(rawHandle, this.#wasm), this.#wasm);
    } catch (error) {
      throw new WasmError(
        `instantiate_protolens failed: ${error instanceof Error ? error.message : String(error)}`,
        { cause: error },
      );
    }
  }

LensHandle wraps a concrete lens with get, put, and law-checking operations:

    try {
      const hintsBytes = packToWasm(hints);
      const rawHandle = wasm.exports.auto_generate_protolens_with_hints(
        schema1._handle.id,
        schema2._handle.id,
        hintsBytes,
      );
      return new ProtolensChainHandle(createHandle(rawHandle, wasm), wasm);
    } catch (error) {
      throw new WasmError(
        `auto_generate_protolens_with_hints failed: ${error instanceof Error ? error.message : String(error)}`,
        { cause: error },
      );
    }
  }

  /** Release the underlying WASM resource. */
  [Symbol.dispose](): void {
    this.#handle[Symbol.dispose]();
  }
}

// ---------------------------------------------------------------------------
// PipelineBuilder — fluent API for constructing protolens chains
// ---------------------------------------------------------------------------

/**
 * Fluent builder for constructing protolens chains from combinator steps.
 *
 * Each method appends a step to the pipeline. Call `build()` to compile
 * the steps into a `ProtolensChainHandle` via the WASM boundary.
 *
 * ```ts
 * const chain = new PipelineBuilder(wasm)
 *   .renameField('post', 'text', 'body')
 *   .addField('post', 'createdAt', 'string')
 *   .build();
 * ```
 */
export class PipelineBuilder {
  readonly #steps: PipelineStep[] = [];
  readonly #wasm: WasmModule;

  constructor(wasm: WasmModule) {
    this.#wasm = wasm;
  }

  /** Rename a field (vertex name + JSON property key). */
  renameField(parent: string, oldName: string, newName: string): this {
    this.#steps.push({ step_type: 'rename_field', parent, name: oldName, target: newName });
    return this;
  }

  /** Remove a field (drop sort with edge cascade). */
  removeField(field: string): this {
    this.#steps.push({ step_type: 'remove_field', name: field });
    return this;
  }

  /** Add a field with a default value. */
  addField(parent: string, fieldName: string, fieldKind: string): this {
    this.#steps.push({ step_type: 'add_field', parent, name: fieldName, kind: fieldKind });
  • ProtolensChainHandle.autoGenerate(schema1, schema2, wasm): Auto-generate a protolens chain between two schemas.
  • ProtolensChainHandle.instantiate(schema): Instantiate the chain against a concrete schema.
  • ProtolensChainHandle.compose(other): Compose with another chain.
  • ProtolensChainHandle.fuse(): Fuse all steps into a single protolens.
  • ProtolensChainHandle.lift(morphismBytes): Lift along a theory morphism.
  • LensHandle.autoGenerate(schema1, schema2, wasm): Auto-generate and instantiate a lens.
  • LensHandle.get(record): Forward projection: extract view and complement.
  • LensHandle.put(view, complement): Backward put: restore from view and complement.
  • LensHandle.checkLaws(instance): Verify GetPut and PutGet laws.

14.9 Schema enrichment

The SchemaBuilder supports enrichment through constraints, which encode defaults, coercions, and merge policies:

// Add constraints to encode enriched schema properties
using schema = protocol.schema()
  .vertex('post:body', 'object')
  .vertex('post:body.text', 'string')
  .edge('post:body', 'post:body.text', 'prop', { name: 'text' })
  .constraint('post:body.text', 'maxLength', '3000')   // refinement type
  .constraint('post:body.text', 'minLength', '1')       // lower bound
  .build();

Constraints serve triple duty: they encode refinement types (value bounds), default behaviors (when a field has a known initial value), and coercion hints (when a field can be safely converted between representations). The protocol’s constraint_sorts array determines which constraint sorts are recognized during compatibility checking.

14.10 Migration analysis

The SDK provides analysis capabilities through the diff/classify pipeline:

// Compute structural diff
const diffReport = panproto.diff(oldSchema, newSchema);

// Classify into breaking vs. non-breaking
const compat = panproto.classify(diffReport);

// Coverage: fraction of source vertices surviving in target
const coverage = compat.non_breaking.length /
  (compat.breaking.length + compat.non_breaking.length);

For optic classification, the protolens chain structure reveals whether a migration is an isomorphism (lossless, rename-only), a lens (lossy, drops data), or more complex:

// Auto-generate protolens
using chain = ProtolensChainHandle.autoGenerate(schema1, schema2, wasm);

// Fuse to analyze the composed transform
using fused = chain.fuse();

// Serialize to inspect the transform structure
const json = fused.toJSON();
// json.complement_constructor === "Empty" means isomorphism
// json.complement_constructor contains "DroppedSortData" means lens

14.11 GAT Engine access

The SDK provides direct access to the GAT engine for theory construction and composition:

// Create theories
using thGraph = panproto.createTheory({
  name: 'ThGraph',
  sorts: [{ name: 'Vertex', params: [] }, { name: 'Edge', params: [] }],
  ops: [
    { name: 'src', inputs: [['e', 'Edge']], output: 'Vertex' },
    { name: 'tgt', inputs: [['e', 'Edge']], output: 'Vertex' },
  ],
  eqs: [],
});

// Compose theories via colimit
using shared = panproto.createTheory({ name: 'Shared', sorts: [{ name: 'Vertex', params: [] }], ops: [], eqs: [] });
using composed = panproto.colimitTheories(thGraph, thConstraint, shared);

14.12 Expression parser & evaluator

The SDK provides three functions for working with the expression language:

  • parseExpr(source: string, wasm: WasmModule): Expr parses Haskell-style source text into an AST.
  • evalExpr(expr: Expr, env: Record<string, Literal> | undefined, wasm: WasmModule): Literal evaluates an expression with optional environment bindings.
  • formatExpr(source: string, wasm: WasmModule): string round-trip formats an expression into canonical form.
import { parseExpr, evalExpr, formatExpr } from '@panproto/core';

const expr = parseExpr('\\x -> x + 1', wasm);
const result = evalExpr(expr, { x: 2 }, wasm);   // 3
const pretty = formatExpr('\\x->x+ 1', wasm);    // "\\x -> x + 1"

14.13 Declarative query API

The executeQuery function runs a declarative query against an instance:

  • executeQuery(query: InstanceQuery, instance: Instance, wasm: WasmModule): QueryMatch[]
  • InstanceQuery type: { anchor, predicate?, groupBy?, projection?, limit?, path? }
  • QueryMatch type: { nodeId, anchor, value, fields }
import { executeQuery } from '@panproto/core';

const matches = executeQuery(
  { anchor: 'post:body', predicate: { field: 'text', op: 'contains', value: 'hello' } },
  instance,
  wasm,
);

14.14 VCS integration

The SDK exposes version control operations for schemas and data:

// Initialize a repository
using repo = panproto.vcsInit('atproto');

// Stage and commit schema changes
panproto.vcsAdd(repo, schema);
const hash = panproto.vcsCommit(repo, 'add post schema', 'author@example.com');

// Query history
const log = panproto.vcsLog(repo, 10);
const status = panproto.vcsStatus(repo);

14.15 Type definitions

The types.ts module defines all TypeScript interfaces that mirror Rust structures. Key types include:

  • WasmExports: The raw WASM module interface listing all entry points
  • ProtocolSpec, Vertex, Edge, HyperEdge, Constraint: Schema domain types
  • MigrationSpec, LiftResult, GetResult: Migration domain types
  • ExistenceReport, ExistenceError: Existence checking results
  • DiffReport, SchemaChange, Compatibility: Schema diff results
  • TheorySpec, SortSpec, OperationSpec, EquationSpec: GAT theory types
  • ProtolensChainSpec, ComplementConstructor: Protolens types
export type Compatibility = 'fully-compatible' | 'backward-compatible' | 'breaking';

/** Schema diff report. */
export interface DiffReport {
  readonly compatibility: Compatibility;
  readonly changes: readonly SchemaChange[];
}

// ---------------------------------------------------------------------------
// Existence checking
// ---------------------------------------------------------------------------

/** A structured existence error. */

14.16 Error hierarchy

The SDK defines a four-class error hierarchy, all extending PanprotoError:

export interface DirectedEquation {
  readonly name: string;
  readonly lhs: Term;
  readonly rhs: Term;
  readonly implTerm: Expr;
  readonly inverse?: Expr | undefined;
}

/** Conflict resolution strategy. */
export type ConflictStrategy =
  | { readonly type: 'keep_left' }
  | { readonly type: 'keep_right' }
  | { readonly type: 'fail' }
  | { readonly type: 'custom'; readonly expr: Expr };

/** Conflict resolution policy. */
export interface ConflictPolicy {
  readonly name: string;
  readonly valueKind: ValueKind;
  readonly strategy: ConflictStrategy;
}

/** Pattern for expression matching. */
export type Pattern =
  | { readonly type: 'wildcard' }
  | { readonly type: 'var'; readonly name: string }
  | { readonly type: 'lit'; readonly value: Literal }
  | { readonly type: 'record'; readonly fields: readonly [string, Pattern][] }
  | { readonly type: 'list'; readonly items: readonly Pattern[] };

/** Expression in the pure functional language. */
export type Expr =
  | { readonly type: 'var'; readonly name: string }
  | { readonly type: 'lam'; readonly param: string; readonly body: Expr }
  | { readonly type: 'app'; readonly func: Expr; readonly arg: Expr }
  | { readonly type: 'lit'; readonly value: Literal }
  | { readonly type: 'record'; readonly fields: readonly [string, Expr][] }
  | { readonly type: 'list'; readonly items: readonly Expr[] }
  | { readonly type: 'field'; readonly expr: Expr; readonly name: string }
  | { readonly type: 'index'; readonly expr: Expr; readonly index: Expr }
  | { readonly type: 'match'; readonly scrutinee: Expr; readonly arms: readonly [Pattern, Expr][] }
  | { readonly type: 'let'; readonly name: string; readonly value: Expr; readonly body: Expr }
  • PanprotoError: Base class for all panproto errors.
  • WasmError: Errors from the WASM boundary (load failures, call failures, disposed handles).
  • SchemaValidationError: Schema building errors, with a list of individual error strings.
  • MigrationError: Migration compilation or composition errors.
  • ExistenceCheckError: Existence check failures, bundling the full ExistenceReport.

14.17 Usage pattern

A typical SDK session:

import { Panproto } from '@panproto/core';

// Initialize (loads WASM)
using panproto = await Panproto.init();

// Get a protocol
const atproto = panproto.protocol('atproto');

// Build schemas
using oldSchema = atproto.schema()
  .vertex('post', 'record', { nsid: 'app.bsky.feed.post' })
  .vertex('post:body', 'object')
  .edge('post', 'post:body', 'record-schema')
  .build();

using newSchema = atproto.schema()
  .vertex('post', 'record', { nsid: 'app.bsky.feed.post' })
  .vertex('post:body', 'object')
  .vertex('post:body.tags', 'array')
  .edge('post', 'post:body', 'record-schema')
  .edge('post:body', 'post:body.tags', 'prop', { name: 'tags' })
  .build();

// Compile migration
using migration = panproto.migration(oldSchema, newSchema)
  .map('post', 'post')
  .map('post:body', 'post:body')
  .compile();

// Transform records
const result = migration.lift(inputRecord);
Tip

The using keyword (TC39 Explicit Resource Management) ensures all WASM handles are freed when the variables go out of scope. If your runtime doesn’t support using, call [Symbol.dispose]() manually or use a try/finally block.