npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@semverse/sdk

v0.0.3

Published

TypeScript Semantic client, typed values, and Node process plugin SDK

Readme

@semverse/sdk

TypeScript types, builders, code generation, value codecs, and HTTP and file clients and Node process plugins for Semantic.

The SDK has two layers:

  • An ergonomic client layer for application code: SemanticClient, transports, file operations, package builders, and JavaScript-friendly values.
  • Schema tooling for package authors: complete package/query/type definitions, generated definitions for Semantic's built-in packages, and a package-to-TypeScript generator.

The main package and the programmatic generator work in modern browsers and in Node.js 20 or newer. The command-line generator and repository regeneration script are Node-only.

Contents

Install

npm install --save-exact @semverse/[email protected]

Node.js applications that want to run the Rust application and storage in-process can instead add @semantic/embedded. It returns this package's SemanticClient without starting an HTTP server; the SDK itself remains native-free and portable.

The package is ESM-only and includes its TypeScript declarations. There is no default export.

Node process plugins import their runtime from @semverse/sdk/plugin. Published packages include the canonical import contract used by self-describing plugins.

Quick start

import {
  HttpTransport,
  SemanticClient,
  query,
  expr,
  projection,
} from "@semverse/sdk";

interface Post {
  title: string;
  published: boolean;
  views: number | bigint;
}

const client = new SemanticClient(
  new HttpTransport("http://localhost:8888/api/v1/rpc"),
);

await client.insert<Post>(
  "post-1",
  { title: "Hello, Semantic", published: true, views: 0 },
  { collection: "posts" },
);

const result = await client.query<Post>(
  query.select("posts", {
    projection: ["title", "published", "views"].map((name) =>
      projection.field(expr.field(name)),
    ),
  }),
);

if (result.kind === "select") {
  for (const post of result.rows) console.log(post.title);
}

Generic result types describe the object stored inside an entity or returned as a query row. They provide static checking only; the SDK does not validate server data against them at runtime.

Runtime support

The package targets ES2022 and requires:

  • Node.js 20 or newer, or a modern browser.
  • The Fetch API (fetch, Headers, Request, and Response) for HTTP and file operations. Node.js 20 provides it globally.
  • structuredClone when using builders or package serialization.

The main @semverse/sdk entry point and @semverse/sdk/generator contain no Node-only imports. Only the semantic-ts-generate CLI and the repository's regeneration script use Node filesystem and process APIs.

Browser applications must be able to reach the Semantic server; normal browser origin, CORS, mixed-content, and credential rules still apply.

Transports

SemanticClient operates on the small RpcTransport interface. Use the built-in HTTP transport, or provide your own implementation.

HTTP

HTTP is the simplest choice for ordinary request/response use. Headers and an abort signal can be configured on the transport and apply to every invocation. Each operation also accepts signal; either signal can cancel that operation. The SDK imposes no timeout policy.

import { HttpTransport, SemanticClient } from "@semverse/sdk";

const controller = new AbortController();
const transport = new HttpTransport("https://semantic.example/api/v1/rpc", {
  headers: {
    authorization: `Bearer ${accessToken}`,
    "x-client-name": "example-web-app",
  },
  signal: controller.signal,
});
const client = new SemanticClient(transport);

const pending = client.sql("SELECT * FROM notes");

// Cancels this and any other in-flight request using this transport. Once the
// signal is aborted, construct a new transport before making more requests.
controller.abort();
await pending.catch(() => undefined);

You can inject a Fetch-compatible function with the fetch option, which is useful for testing or centrally adding credentials and observability.

Custom transports

import {
  SemanticClient,
  type RpcTransport,
  type SemanticValue,
} from "@semverse/sdk";

const transport: RpcTransport = {
  async invoke(
    command: string,
    payload: SemanticValue,
  ): Promise<SemanticValue | undefined> {
    // Send command and payload through an application-specific channel.
    return undefined;
  },
};

const client = new SemanticClient(transport);

Custom transports receive decoded SemanticValues. The built-in transports handle RPC envelopes and tagged-value encoding themselves.

Queries and mutations

Construct a typed AST directly and send it through client.query(). The server executes it without converting it to SQL or parsing text. ASTs cover selects, joins, aggregates, subqueries, inserts, updates, deletes, and every supported DDL operation.

import {
  query,
  expr,
  projection,
  orderBy,
  assignment,
  fieldPath,
} from "@semverse/sdk";

const byOwner = query.select("notes", {
  predicate: expr.eq(expr.field("owner"), expr.parameter("owner")),
  projection: [
    projection.field(expr.field("title")),
    projection.wildcard("details"),
  ],
  order_by: [orderBy(expr.field("created_at"), "desc")],
  limit: expr.literal(20),
});
await client.query(byOwner, {
  params: { owner: "alice" },
  scopeId: "workspace-1",
});
await client.query(byOwner, {
  params: { owner: "bob" },
  scopeId: "workspace-1",
});

await client.query(
  query.insert("notes", [{ id: "note-1", title: "New note" }]),
);
await client.query(
  query.update(
    "notes",
    [assignment(fieldPath("title"), expr.parameter("title"))],
    { predicate: expr.eq(expr.field("id"), expr.literal("note-1")) },
  ),
  {
    params: { title: "Updated note" },
  },
);
await client.query(
  query.ddl({
    upsert_collection: {
      name: "notes",
      kind: "untyped",
      integrity_mode: "permissive",
    },
  }),
);

Builders are optional; ordinary objects matching the exported Query, Expr and DdlOperation types use the same representation. Query fields with schema defaults can be omitted. Field paths contain explicit segments: expr.field("profile", "name") selects a nested field, while expr.field("profile.name") names a single field. The complete transitive DDL schema is exported under QuerySchema and @semverse/sdk/query.

AST parameters stay unbound and bindings are supplied separately. Names are case-sensitive ASCII identifiers without a leading colon; repeated names reuse the same typed value. Missing, unused or invalid bindings return RpcError with code query_parameter_error and data.reason (plus data.name when applicable). Bindings use the tagged value codec, including UUIDs, bytes and wide integers.

SQL parsing fallback

When the starting input is SQL text, parseSql() returns a reusable AST without executing it. The selected scope determines the SQL dialect. Parameters remain unbound; values are provided when the returned AST is executed.

const parsed = await client.parseSql(
  "SELECT id FROM notes WHERE owner = :owner",
  {
    scopeId: "workspace-1",
  },
);
await client.query(parsed, {
  params: { owner: "alice" },
  scopeId: "workspace-1",
});

Parsing uses exact value decoding so literal widths and scalar kinds survive resubmission. Numeric and other typed scalar fields can be TaggedScalar wrappers; string, boolean and null fields retain their ordinary JavaScript representation. Void remains an explicit wrapper, including inside literal objects. Both client.parseSql() and client.invoke(commands.parseSql, ...) select exact decoding. Custom transports must honor InvokeOptions.valueDecoding for the same guarantee.

SQL and PRQL

sql() and prql() remain available for text queries. All accept an optional scopeId; SQL accepts the same separate parameter bindings as AST queries. A format option is only valid when query() receives text.

const sqlResult = await client.sql<{ id: string; title: string }>(
  "SELECT id, title FROM notes",
  { scopeId: "workspace-1" },
);

const prqlResult = await client.prql<{ title: string }>(
  "from notes | select {title}",
);

const sameAsSql = await client.query("SELECT * FROM notes", {
  format: "sql",
});

QueryResult<T> is a discriminated union. Check kind before accessing operation-specific fields such as rows, returning, inserted, or deleted. Counts may be bigint when they exceed JavaScript's safe integer range.

Get, insert, and delete

interface Note {
  title: string;
  note_content: string;
  note_format: "text" | "markdown";
}

await client.insert<Note>(
  "note-1",
  {
    title: "SDK notes",
    note_content: "Typed from end to end",
    note_format: "markdown",
  },
  { collection: "notes", scopeId: "workspace-1" },
);

const entity = await client.get<Note>("note-1", {
  collection: "notes",
  scopeId: "workspace-1",
});

if (entity) console.log(entity.id, entity.collection, entity.object.title);

await client.delete("note-1", {
  collection: "notes",
  scopeId: "workspace-1",
});

The collection is optional for these methods and is omitted from the RPC payload when not supplied.

Batch operations

BatchBuilder takes snapshots of supplied objects and of the completed operation list, so later mutation of inputs does not modify an already built batch.

import { BatchBuilder } from "@semverse/sdk";

const operations = new BatchBuilder()
  .upsert("note-1", { title: "First" }, "notes")
  .upsert("note-2", { title: "Second" }, "notes")
  .delete("obsolete-note", "notes")
  .deleteMany(["draft-1", "draft-2"], "notes")
  .build();

const outcome = await client.batch(operations, { scopeId: "workspace-1" });
console.log(outcome.stats.upserted, outcome.stats.deleted);

By default the result contains { dataset, stats }. Select a compact response with returning; its literal value determines the TypeScript result type:

const { stats } = await client.batch(operations, { returning: "stats" });
const { changes } = await client.batch(operations, { returning: "changes" });
const { rows } = await client.batch(operations, {
  returning: { projection: { fields: ["title"] } },
});

changes contains { collection, id, kind: "upsert" | "delete" }, sorted by collection and ID. It describes net committed changes, so unchanged rows and create-then-delete sequences are absent. Stats retain operation counts. Projection also includes changes and returns { collection, id, object } for surviving changed rows, with only selected canonical attribute keys; absent values are omitted. An empty field list returns identities with empty objects. Unknown/ambiguous fields, duplicate fields (including aliases), and invalid modes fail before commit with batch_return_error and data.reason (unknown_field, duplicate_field, or unknown_mode). Explicit "dataset" retains the default shape. commands.batch keeps its dataset-only contract; commands.batchReturning accepts all modes and returns the reply union.

The remotely callable BatchOperation deliberately includes only the operations accepted by the current application RPC command: create, upsert, delete by ID, and delete by multiple IDs. The reflected core schema also describes query-based update and delete variants, but SemanticClient.batch() does not expose them as supported remote operations.

Commands and scopes

command<P, O>() creates a typed command descriptor. It has no runtime schema or validation; it associates a command name with compile-time payload and output types.

import { command } from "@semverse/sdk";

const greet = command<{ name: string }, { message: string }>("example.greet");
const response = await client.invoke(greet, { name: "Ada" });
console.log(response.message);

Generated package commands use the same descriptors. The main entry point also exports descriptors for the built-in server commands as commands.

import { commands } from "@semverse/sdk";

const scope = await client.invoke(commands.scopeOpen, {
  uri: scopeUri,
  mode: "auto_create",
  set_current: false,
});

const notes = await client.sql("SELECT * FROM notes", {
  scopeId: scope.scope_id,
});

Explicit scopeId options are available on all high-level database methods, package upserts, and file operations. The commands object additionally exposes scopeOpen, scopeUse, scopeCurrent, and scopeList for lifecycle management. Which scope URI schemes and visibility modes are usable depends on the server.

Files

Uploads are create-only: an existing explicit or automatic hash identity returns RpcError with code file_already_exists. Native bytes use immutable generated hash locators; custom upload locators are rejected, while existing locators remain readable. The separate importer publication operation retains upsert behavior.

Use await files.delete(id, { scopeId, signal }) for explicit native deletion. It succeeds if already absent and returns file_referenced when an enforced reference survives. Removing a domain file link does not delete the native file. Deletion atomically records cleanup intent with metadata removal. Bytes are currently retained: destructive cleanup is disabled until physical store ownership across scopes and a retention policy can be guaranteed.

File transfer uses a separate HTTP endpoint and FileClient. Use createHttpClient to share Fetch, base headers, credentials, and a connection abort signal across RPC, upload, and download. Protocol headers for each operation override base headers. Set fileEndpoint if the file endpoint cannot be derived.

import { createHttpClient } from "@semverse/sdk";

const rpcEndpoint = "https://semantic.example/api/v1/rpc";
const { client, files } = createHttpClient(rpcEndpoint, {
  headers: { authorization: `Bearer ${accessToken}` },
  credentials: "include",
});

const uploaded = await files.upload({
  content: new Blob(["Hello from Semantic\n"], { type: "text/plain" }),
  filename: "hello.txt",
  mimeType: "text/plain",
  scopeId: "workspace-1",
  entity: { title: "Greeting", description: "Created by the SDK" },
  onProgress(uploadedBytes, totalBytes) {
    console.log(uploadedBytes, totalBytes);
  },
});

const complete = await files.read(uploaded.id, { scopeId: "workspace-1" });
const firstKilobyte = await files.read(uploaded.id, {
  scopeId: "workspace-1",
  offset: 0,
  size: 1024,
});
const downloadUrl = files.url(uploaded.id, "workspace-1");

// Bounded-memory consumption; cancellation also releases the response body.
const controller = new AbortController();
const { stream, contentLength, totalSize } = await files.readStream(
  uploaded.id,
  {
    scopeId: "workspace-1",
    signal: controller.signal,
  },
);
await stream.pipeTo(destination);

content accepts any Fetch BodyInit, including Blob, Uint8Array, and ReadableStream. For a stream, the SDK sets duplex: "half", as required by Node's native Fetch implementation and accepted by supporting browsers.

onProgress currently reports upload lifecycle boundaries (start and completion), not incremental network byte progress. read buffers the entire response; readStream returns a ReadableStream<Uint8Array> with status and optional content type, content length, content range, and total size. A zero-sized read returns empty content without a request (synthetic stream status 200); 416 also returns empty content. Ranged reads require a valid 206 response and Content-Range. Headers are validated before exposing bytes and declared byte counts are checked at EOF; malformed or truncated responses raise TransportError. Cancel the stream, its reader, or an associated signal when abandoning a download. Signal cancellation rejects with AbortError.

Existing standalone constructors and Fetch wrappers remain supported:

const authenticatedFetch: typeof fetch = (input, init) => {
  const headers = new Headers(init?.headers);
  headers.set("authorization", `Bearer ${accessToken}`);
  return fetch(input, { ...init, headers });
};

const files = new FileClient(
  "https://semantic.example/api/v1/file",
  authenticatedFetch,
);

Process plugins

The self-describing plugin API below is unreleased and requires a matching Semantic server checkout.

@semverse/sdk/plugin is the Node-only adapter for semantic.import/v1 over Content-Length framed stdio (semantic.plugin.v1, interface protocol v2). Keep stdout reserved for the protocol; write diagnostics to stderr.

import { definePlugin, importExports, servePlugin } from "@semverse/sdk/plugin";
import { packageDefinition } from "./package.js";

const plugin = definePlugin({
  package: packageDefinition,
  exports: {
    source: importExports.source({ describe: sourceDescription, probe }),
    fetcher: importExports.fetcher({ source: "source", fetch }),
  },
  configure(configuration, { signal, host }) {
    return parseConfiguration(configuration);
  },
});
await servePlugin(plugin);

The package is an ordinary Package (exported from @semverse/sdk/core and @semverse/sdk/plugin) with a required name and version. The SDK attaches the semantic.plugin annotation, pins foreign import-interface fingerprints, and answers protocol Describe before the live handshake. No command-line discovery or activation descriptor is used.

semantic api plugin describe --stdio node -- /absolute/path/to/dist/plugin.js
semantic api plugin install --stdio node --config config.json -- /absolute/path/to/dist/plugin.js
semantic api plugin list

Use implement({package, module, name}, handlers) for an interface defined in the plugin package. Its methods receive positional arguments and a context containing configuration, signal and host. The host fills own-package fingerprints. Annotate interface method metadata with semantic.command to expose a method through semantic.plugin.invoke or semantic.plugin.invoke_stream. Existing command grants authorize the virtual package.export.method name.

host.call(command, payload) invokes a unary host command; host.stream(command, payload, input?) consumes a host stream. Both run as the plugin service account without caller delegation. Permission requests are metadata and are never automatically granted.

A configuration TypeDef can be named with configuration: "Configuration". configure(configuration, {signal, host}) runs before readiness and its return value becomes context.configuration. Use dispose(configuration) for process-owned resources after all handlers and streams finish cleanup.

Source.describe can also be a function receiving context. A Fetcher implements fetch(request, context). An Importer implements both importSource(request, context) and importFetched(request, content, context); each operation returns an async iterable of ContentEvent (optionally through a Promise). Each Fetcher/Importer names its describing Source through source. Multiple named exports and multiple Sources are supported.

Source.probe(request, operation, context) may return { status: "supported", options_class: "my:plugin:ImportOptions" }. Install that class and its attributes in the plugin package's migrations. The selection may depend on the URL and operation, for example different settings for a video and a channel. The import UI renders the class's normal generated form, and the host validates its value before forwarding it as request.options. Quick import uses valid form defaults; required fields without defaults open the review form. Form values are keyed by attribute ID; accept those keys alongside any plain option names your API callers use.

request.skip_contents asks for metadata only: omit media contents such as downloads. The host stores the flag on imported items and forwards it again when an item is re-imported. Entities and file_start events may name a source, the URL that re-imports that item alone; the host stores it as source_url (the request URL otherwise) next to imported_by and imported_at. Strict classes accept this provenance by extending semantic:import:imported.

Content is serial: yield an entity, or file_start, zero or more file_bytes containing Uint8Array, then file_end. File metadata uses key, mime_type, optional filename, optional expected_size (safe integer or bigint), and optional attributes. The SDK checks file order/size and automatically emits a terminal {items, bytes} summary. Do not yield the summary yourself. Incoming content is an async iterable with summary: Promise<ContentSummary>; consume it to completion before awaiting the summary. Scalar attributes retain exact tags through transformations, including UUIDs and wide integers. Configuration and request options also preserve scalar tags; strings, booleans and null unwrap. Use the existing value constructors for explicitly typed attributes and decodeTagged(encodeTagged(v)) when convenient scalar decoding is needed.

stableEntityId(namespace, source, key) matches Rust's length-delimited UTF-8 hash and UUID bits. It is the default host identity when attributes.id is absent. On hosts supporting explicit import IDs, set a nonempty string attributes.id to reuse upstream identities across source URLs, and use those same ID strings for references. IDs remain subject to ordinary caller permissions and cannot reuse a visible entity of a different class. sourceProvenance returns the three ordinary semantic:import:source_* attributes; merge them into proposals where needed. Entity identity is independent of plugin revision and file content.

The host uploads completed files through its regular file service, then commits entity proposals and file parent updates in one DB batch after the stream succeeds. Thus a post can reference its file in contents while the file refers back to the post in parent. Failure discards pending entity/parent changes; completed file uploads, including replacements, remain. File bytes are streamed, while entity metadata is retained until the final batch.

Set primary: true on the main entity proposal to choose the full card shown after import (for example the post for a video, or the account for a channel). Only one distinct proposal key may be primary. A successful import retains {items, bytes, main_entity?, entities} on its job, using host-resolved entity IDs. semantic.jobs.watch is a streaming command taking {id, scope_id?}; it replays the current job, emits persisted status updates, and ends with the final job record on success, failure, or cancellation. Reconnect to this same command to resume observation without submitting another import.

The adapter advances output iterators only under host credit (maximum 64 events), requests incoming content one event at a time, and respects writable backpressure. It supports same-session idle-stream forwarding. Call and stream cancellation abort context.signal; wrap acquired resources in try/finally and pass the signal to network requests. Cleanup must cooperate with cancellation: shutdown waits for in-flight handlers and iterator cleanup before disposal and shutdown_ack. EOF and protocol failures also abort and clean up work. servePlugin accepts optional input, output, signal, and maxFrameBytes (default 64 MiB). Yield file bytes in small chunks; the frame limit is not a total file size limit. This adapter implements the import interfaces; arbitrary host callbacks and WebSocket serving are outside its API.

Values and serialization

SemanticValue

SemanticValue is the JavaScript-facing value model used by entities and custom commands. It includes ordinary primitives, bigint, Uint8Array, Date, arrays, objects, Map, Semantic variants, and explicit tagged values.

The automatic RPC mapping includes:

| JavaScript value | RPC value | | ---------------------------------------- | -------------------------------- | | undefined, null, boolean, string | void, null, bool, string | | Integer number or bigint | i64 by default | | Non-integer number | f64 | | Uint8Array | bytes | | Date | nanosecond date_time | | Array, Map, plain object | list, map, object | | { $variant, value, type? } | variant |

Decoded integers are number when safely representable and bigint otherwise. A decoded date_time is its numeric nanosecond representation; decoding does not automatically construct a Date.

Exact scalar tags

JavaScript cannot distinguish a UUID from a string or u8 from i64. Use the value helpers when the exact Semantic scalar kind matters:

import { decodeTaggedExact, encodeTagged, value } from "@semverse/sdk";

const payload = {
  id: value.uuid("0f8fad5b-d9cb-469f-a165-70867728950e"),
  priority: value.int("u8", 7),
  elapsed: value.durationMs(250),
  state: value.variant("published", null, "PostState"),
};

const tagged = encodeTagged(payload);
const exact = decodeTaggedExact(tagged);

encodeTagged() and the built-in transports recognize values produced by these helpers. Integer helpers validate both the declared width and JavaScript safe-number rules. decodeTaggedExact() recursively preserves scalar tags as explicit wrappers; ordinary decodeTagged() returns convenient JavaScript scalars.

Three JSON domains

Similar-looking JSON appears in three different wire contexts. Use the matching codec rather than applying one representation everywhere.

| Codec | Use | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | rpcJson / stringifyJson / parseJson | Lossless JSON tokens in RPC envelopes. The transport separately applies tagged-value encoding. | | facetValueJson | A standalone externally tagged Semantic value. | | packageJson | Facet package documents; it tags only schema locations that contain Semantic values, such as defaults, examples, expressions, and migration objects. | | flatJson | Ordinary untagged JSON used for file entity metadata. |

import {
  facetValueJson,
  flatJson,
  packageJson,
  parseJson,
  stringifyJson,
} from "@semverse/sdk";

const exactInteger = stringifyJson({ sequence: 9_007_199_254_740_993n });
const parsed = parseJson(exactInteger); // unsafe integer tokens become bigint

const valueDocument = facetValueJson.stringify(value.uuid(uuid));
const packageDocument = packageJson.stringify(pkg);
const metadataDocument = flatJson.stringify({
  title: "Photo",
  taken: new Date(),
});

stringifyJson() emits bigint as an exact JSON number token rather than a quoted string. flatJson turns Date into an ISO string and bytes into number arrays; it rejects explicit tagged wrappers because they have no flat representation.

Defining packages

The exported interfaces let applications construct a package directly. Builders fill repetitive container/default fields, reject duplicate definitions, clone inputs, and return detached snapshots. They do not infer schema IDs or validate a package against a server catalog.

This example defines a type, attribute, class, contract command, secondary module, DDL migration, and seed entity:

import {
  PackageBuilder,
  emptyMeta,
  type Package,
  type TypeNode,
} from "@semverse/sdk";

const stringType: TypeNode = {
  kind: { string: { format: null, normalization: null } },
  constraints: [],
  annotations: [],
};

const pkg: Package = new PackageBuilder("acme.blog", "blog")
  .version({ major: 1, minor: 0, patch: 0, pre: null, build: null })
  .meta({ title: "Acme Blog", description: "Blog schema and commands" })
  .root((module) => {
    module.type({
      name: "Slug",
      module: null,
      params: [],
      ty: stringType,
      visibility: "public",
      meta: emptyMeta(),
    });

    module.attribute("title", {
      id: "acme.blog/title",
      name: "title",
      ty: stringType,
      constraints: [],
      meta: emptyMeta(),
    });

    module.class("Article", {
      id: "acme.blog/Article",
      name: "Article",
      inherits: null,
      extends: [],
      "semantic:class:strict_schema": true,
      attributes: {
        title: {
          attribute: { id: "acme.blog/title" },
          required: true,
          ui_order: 0,
          computed: null,
          constraints: [],
          meta: emptyMeta(),
        },
      },
      constraints: [],
      meta: emptyMeta(),
    });

    module.contract("api", (contract) => {
      contract.command("acme.blog.article.get", {
        params: [{ name: "slug", ty: stringType }],
        results: [stringType],
        throws: null,
        async_fn: true,
      });
    });
  })
  .module("admin", (module) => {
    module.meta({ description: "Administrative definitions" });
  })
  .migration("blog", "0001-create-articles", (migration) => {
    migration
      .description("Create and seed the article collection")
      .operation({
        ddl: {
          upsert_collection: {
            name: "articles",
            kind: "schema",
            integrity_mode: "strict_registered_schema",
          },
        },
      })
      .insert("articles", "welcome", { title: "Welcome" });
  })
  .build();

await client.upsertPackage(pkg, { scopeId: "workspace-1" });

root() and module() configure a ModuleBuilder; contract() configures a ContractBuilder; and migration() configures a MigrationBuilder. Lower-level types such as TypeNode, TypeDef, AttributeType, ClassType, FunctionType, MigrationOperation, and query AST types are all exported for package authors who need more control.

Generating TypeScript

The generator converts a Package into declarations for its modules, contracts, types, attributes, classes, interfaces, and commands. It resolves lexical and qualified references, supports generics, avoids TypeScript identifier collisions, and emits class references as entity IDs (string) rather than nested objects.

CLI

semantic-ts-generate package.json src/generated/acme-blog.ts

When working in this repository:

npm run generate:package -- package.json src/generated/acme-blog.ts

The input must be a JSON representation of Package; packageJson.stringify(pkg) produces the appropriate Facet package representation. The CLI parses integers losslessly and formats the generated TypeScript with Prettier.

A generated module exports:

  • packageName, a string literal containing the package name.
  • Runtime constants for attribute and class IDs.
  • Type aliases for package declarations and classes.
  • Typed command descriptors ready for client.invoke().

Generated class fields use their canonical attribute IDs through those constants, so the declared object shape matches database records exactly.

Names that are not valid TypeScript identifiers are sanitized. If two declarations would share a name, the generator qualifies or suffixes them; use the symbols in the generated output rather than predicting their spelling.

Programmatic API

The browser-safe generator accepts an in-memory package:

import { packageModel, renderPackage } from "@semverse/sdk/generator";

const model = packageModel(pkg);
const typescriptSource = renderPackage(model);

packageModel() produces the normalized PackageModel intermediate form; renderPackage() renders that model. The programmatic renderer does not write a file or run Prettier.

Built-in generated types

Canonical definitions generated from the Rust source of truth are checked in with the SDK.

import {
  ATTR_PERSON_DISPLAY_NAME,
  ATTR_NOTE_NOTE_CONTENT,
  ATTR_NOTE_NOTE_FORMAT,
  type Person,
  type Note,
} from "@semverse/sdk/generated/base";
import {
  ATTR_FILE_FILENAME,
  ATTR_FILE_MIME_TYPE,
  type File as SemanticFile,
} from "@semverse/sdk/generated/filestore";

const person: Person = { [ATTR_PERSON_DISPLAY_NAME]: "Ada Lovelace" };
const note: Note = {
  [ATTR_NOTE_NOTE_CONTENT]: "Generated types are ordinary TypeScript types.",
  [ATTR_NOTE_NOTE_FORMAT]: "text",
};
const file: SemanticFile = {
  [ATTR_FILE_FILENAME]: "notes.txt",
  [ATTR_FILE_MIME_TYPE]: "text/plain",
};

The main entry point also exposes the exact Rust-reflected core wire schema under a namespace, while the core subpath provides direct imports:

import { CoreSchema } from "@semverse/sdk";
import {
  ATTR_TITLE,
  type Package as ExactPackage,
  type expression_Expr,
} from "@semverse/sdk/core";

declare const reflected: CoreSchema.Package;
const exact: ExactPackage = reflected;
declare const expression: expression_Expr;
const titleAttribute = ATTR_TITLE;
const sameTitleAttribute = CoreSchema.ATTR_TITLE;

Query types in the main entry point are aliases of the generated Semantic query package, rather than a separate hand-maintained model. Package generation resolves { named: { name, args } } as a type reference; { ref: ... } represents an entity ID and generates string. Existing package definitions that used ref for named schema types should use named. Generated variants honor their schema tagging strategy; externally tagged unit cases are strings and payload cases are single-key objects. Exact scalar wrappers are included in generated scalar field types.

Use the main entry point's ergonomic types for normal client/package authoring and the reflected core types when exact correspondence with Rust's full schema is important. Some reflected types include server-internal variants that the ergonomic client intentionally does not accept.

Errors

import { ProtocolError, RpcError, TransportError } from "@semverse/sdk";

try {
  await client.sql("SELECT * FROM missing_collection");
} catch (error) {
  if (error instanceof RpcError) {
    console.error(error.code, error.message, error.data);
  } else if (error instanceof TransportError) {
    console.error(error.status, error.message, error.cause);
  } else if (error instanceof ProtocolError) {
    console.error("Malformed RPC response", error.cause);
  } else {
    throw error;
  }
}
  • RpcError represents a structured error returned by the Semantic command and exposes code and optional decoded data.
  • TransportError represents Fetch failures, non-success HTTP statuses, and file transfer errors. HTTP status is available when applicable.
  • ProtocolError represents malformed or inconsistent RPC responses.
  • All three extend SemanticError, which extends Error.

Current limitations

  • Remote batches support create, upsert, and ID-based deletion only.
  • Types and generated command descriptors are compile-time contracts; the SDK does not perform runtime schema validation.
  • The SDK is ESM-only.

Package exports

| Import | Contents | | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | @semverse/sdk | Ergonomic types, codecs, errors, commands, transports, client, builders, files, built-in command descriptors, and CoreSchema/QuerySchema. | | @semverse/sdk/plugin | Node stdio import plugins, descriptors, content types, cancellation, stable IDs, and provenance. | | @semverse/sdk/query | Canonical AST and transitive DDL schema generated from the Semantic query package. | | @semverse/sdk/core | Direct exports of the complete Rust-reflected core schema. | | @semverse/sdk/generator | Browser-safe package model and rendering API. | | @semverse/sdk/generated/base | Generated types for semantic.base. | | @semverse/sdk/generated/filestore | Generated types for semantic.filestore. | | @semverse/sdk/generated/commands | Built-in typed command descriptors; these are also exported as commands from the main entry point. |

The wildcard @semverse/sdk/generated/* export also makes additional checked-in generated modules importable by subpath.

Development

From lib/js/sdk:

npm ci
npm run build
npm test
npx prettier --check .

The checked-in core.ts, base.ts, and filestore.ts files are generated from Rust reflection and canonical package values through the semantic_sdk_export crate:

npm run generate        # rewrite generated files
npm run generate:check  # fail if checked-in files are stale

Do not edit those generated files directly. generate requires a working Rust workspace in addition to Node dependencies.

Publishing

Build and inspect the distributable from this directory, then publish that exact tarball to npm:

npm ci
npm test
npm pack --dry-run
npm pack --pack-destination /tmp
npm publish /tmp/semverse-sdk-0.0.3.tgz --access public

Packing builds the SDK automatically. The archive contains ESM entry points, TypeScript declarations, the generator CLI, and the plugin import contract; repository tests and the Rust-dependent regeneration script are excluded.

The plugin adapter includes a real Rust-host interoperability test. After building the SDK, run from the workspace root (use nix develop --command when Nix is available):

cargo test --quiet --message-format=short -p semantic_import --test typescript_sdk
cargo test --quiet --message-format=short -p semantic_import --test typescript_sdk -- --ignored

The first command compares checked-in fingerprints and Unicode stable-ID vectors to current Rust implementations. To intentionally regenerate the contract, set UPDATE_TS_PLUGIN_CONTRACT=1 for that command and rebuild the SDK. The ignored integration test requires Node and built SDK output; it launches the Node fixture with the production Rust stdio provider, applies canonical interface conformance, and checks Source/Fetcher/Importer, both stream directions, forwarding, typed values, references/provenance, summaries, cancellation and cleanup.