Node

You defined a data model in DSM and ran Kibo over it. What you got back — and what you work against every day — is a typed TypeScript package named after your model. You almost never call the @digitalsubstrate/dsviper binding directly: you import a couple of its modules and let them delegate to the runtime for you.

This page is that daily surface, end to end. For why the generated code exists (the adapter/Dual Reality pattern), see kibo-template-viper; for the raw runtime this layer sits on, see the dsviper for Node chapter — starting at Types and Values.

Anatomy of a generated package

A generated package — say model/ — compiles from a handful of TypeScript modules under src/:

model/
├── src/
│   ├── data.ts                 ← the types: one class per concept, struct, enum, container
│   ├── attachments.ts          ← the verbs on a commit state (one object per attachment)
│   ├── database_attachments.ts ← the same verbs for a standard (non-commit) Database
│   ├── definitions.ts          ← the runtime type catalog (plumbing)
│   ├── value_type.ts           ← Viper type handles used by data / attachments (plumbing)
│   ├── path.ts                 ← field paths used by the fine-grained setters (plumbing)
│   ├── resources.ts            ← the packed definitions blob (plumbing)
│   └── index.ts                ← the barrel: re-exports everything
├── package.json
└── tsconfig.json

This guide uses the versioned commit database throughout; database_attachments.ts is the twin of attachments.ts for the plain, non-versioned Database store — the same verb objects, called directly on a Database instead of a commit state (see Database).

Every file carries this header — it is generated, and regenerated on every model change:

// Generated by kibo from <namespace>. Do not edit by hand.

Treat the package as a build artifact: never hand-edit it, never commit a local tweak — the next generation overwrites you. If something in the generated code is wrong, the fix belongs in the DSM model or the template, not the output.

The two things you import

index.ts re-exports the types at the top level and the verb modules as namespaces, so one import line gets you both:

import {
  Tuto_UserKey, Tuto_Login, Set_Tuto_UserKey,   // the types
  attachments,                                    // the verbs (a namespace)
} from "model";
import { CommitDatabase, CommitStateBuilder, CommitMutableState }
  from "@digitalsubstrate/dsviper";

A first program touches nothing but those, plus a CommitDatabase:

const db = CommitDatabase.open("model.cdb");

// a typed key + a typed struct — from `data`, never a raw dsviper.Value
const user = Tuto_UserKey.create();
const login = new Tuto_Login();
login.nickname = "alice";

// a mutation verb — the attachment's object, applied to the mutating state
const state = new CommitMutableState(CommitStateBuilder.initialState(db));
attachments.user_Login.set(state.attachmentMutating(), user, login);
db.commitMutations("Add user", state);

That is the whole shape of daily work: types are classes you new, changes go through an attachment’s verb object, the commit loop lives on CommitDatabase. The rest of this page is those three ideas in detail.

Where each name comes from

The generated names are mechanical — which is what makes a stack trace legible (see Reading a repro):

You see

It came from

DSM

Tuto_UserKey

a concept

concept User; in namespace Tuto

Tuto_Login, .nickname

a struct and its field

struct Login { string nickname; }

Tuto_Status.ACTIVE

an enum case

enum Status { …, active, }

attachments.user_Login.set

an attachment

attachment<User, Login> login;

Types are prefixed <Namespace>_; an attachment’s object is <key-concept>_<attachment> (lower-first, so user_Login) — so a symbol in an error points straight back to a line of DSM.

The proxy posture

The generated classes — Tuto_UserKey, Tuto_Login, Set_Tuto_UserKey — are proxies. It is tempting to treat them as ordinary objects and drop them into ordinary JS collections. Don’t. Understanding why keeps the runtime boundary clean.

One value, two realities

Viper is a strongly-typed data model. The type catalog, built from your DSM at runtime, is the single source of truth — a value either conforms to its type or it is rejected, at the moment it is built. That safety lives in the runtime.

A proxy is how that runtime value is surfaced to TypeScript — the same value seen two ways (the Dual Reality):

  • Runtime Reality — the actual typed value the engine manipulates and persists.

  • Developer Reality — the proxy your editor autocompletes and tsc type-checks.

Unlike a dynamically-typed binding, where the proxy’s job is to add enforcement the language lacks, here tsc already checks the proxy surface at compile time. The proxy’s job is therefore twofold: give the compiler a typed, idiomatic surface, and carry the underlying runtime value — with its reference semantics, identity, and serialization — so it can cross into the database.

What vprValue is

Every proxy holds exactly one runtime value in a vprValue field:

const user = Tuto_UserKey.create();
user.vprValue;          // the underlying dsviper.ValueKey — the runtime reality

vprValue is the seam between the two realities. The generated code reaches through it to delegate every operation to the runtime — that is its job. It is an implementation detail, not an interchange format for your code. Writing .vprValue in your own logic is a signal that you dropped the proxy posture upstream and are patching the boundary by hand.

Native collection vs. proxy collection

They look interchangeable and are not — but in TypeScript the difference bites at compile time, not at run time:

// A native JS Set, typed by the compiler — but it is a JS collection of proxies,
// not a runtime set<User> the engine can store in an attachment:
const raw = new Set<Tuto_UserKey>();
raw.add(user);

// The proxy collection IS the runtime value:
const strict = new Set_Tuto_UserKey();
strict.add(user);
// strict.add("nope");   // ✗ compile error — add(value: Tuto_UserKey)

Because the proxy surface is typed, tsc rejects a wrong element before you run — there is no deferred, dynamically-typed bug to defend against here. The reason to reach for Set_Tuto_UserKey over a native Set<Tuto_UserKey> is not enforcement, it is that the former is a runtime set<User> you can hand straight to an attachment; the latter is a JS collection you would have to convert at the boundary.

The rule: produce the runtime value at creation

Don’t build a native collection of proxies and convert it at the edge; produce the proxy collection where the value is created:

function generate(): Set_Tuto_UserKey {
  const result = new Set_Tuto_UserKey();
  result.add(Tuto_UserKey.create());
  result.add(Tuto_UserKey.create());
  return result;                       // ready for an attachment — no conversion
}

Stay in the proxy posture: the generated SDK expresses the runtime’s types and carries its values. A native JS structure of proxies gives you neither the runtime value nor a shorter path — only a conversion to write at the boundary.

Living in the proxies

Everything in data follows a few shapes. Once you know them, a new model reads itself.

Keys — concepts and clubs

A concept or club becomes a …Key class. A key is an identity, not data:

const user = Tuto_UserKey.create();   // a fresh identity
user.instanceId();                     // its UUID
user.description();                    // "<uuid>:Tuto::UserKey"
user.equals(other);                    // identity comparison, via the runtime

Use .equals(), not ===: two proxies wrapping the same runtime value are equal by value, not by reference.

Structs — fields as accessors

A struct becomes a class you new, with a typed getter/setter per field:

const login = new Tuto_Login();
login.nickname = "alice";
login.password = "s3cret";
// login.nickname = 42;   // ✗ compile error — nickname is a string
login.copy();             // a deep, independent copy

Enums — cases as static members

const account = new Tuto_Account();
account.state = Tuto_Status.ACTIVE;
account.state.name();              // "active"
Tuto_Status.fromStr("pending");    // parse from a name

Containers — idiomatic, but still runtime values

vector / set / map / xarray / optional become classes with the idiomatic surface — for..of, .size, set operators — all delegating to the runtime:

const keys = new Set_Tuto_UserKey();
keys.add(Tuto_UserKey.create());
keys.size;                         // number of elements
keys.union(other);                 // typed set algebra
for (const k of keys) {            // yields Tuto_UserKey proxies, not raw values
  // …
}

The rule from the proxy posture applies throughout: elements are proxies too. Build and pass the typed container; don’t assemble a native Set/Array of proxies and convert at the boundary.

Mutating: calling an attachment verb

Data does not live in a key — it hangs off it as attachments. Each attachment generates one object whose methods are the verbs; you call them against a state. A state is a point in the commit DAG (see Commit for the model); here is the loop.

Open a state

const db = CommitDatabase.open("model.cdb");

// a fresh, empty state:
const state = new CommitMutableState(CommitStateBuilder.initialState(db));

// …or the latest committed state of an existing database:
const latest = new CommitMutableState(
  CommitStateBuilder.state(db, db.lastCommitId()));

Write — through attachmentMutating(), then commit

The verb object exposes set, per-field set<Field>, union<Field>, … taking an AttachmentMutating. Nothing is persisted until you commit. A full loop against a fresh in-memory database — a real app opens a .cdb, but the shape is identical (the generated definitions module seeds the schema):

import { CommitDatabase, CommitStateBuilder, CommitMutableState }
  from "@digitalsubstrate/dsviper";
import { Tuto_UserKey, Tuto_Login, attachments, definitions } from "model";

const db = CommitDatabase.createInMemory();
db.extendDefinitions(definitions.definitions());

const user = Tuto_UserKey.create();
const login = new Tuto_Login();
login.nickname = "alice";

const state = new CommitMutableState(CommitStateBuilder.initialState(db));
attachments.user_Login.set(state.attachmentMutating(), user, login);
attachments.user_Login.setNickname(state.attachmentMutating(), user, "alice2"); // fine-grained
db.commitMutations("Register alice", state);

Read — through attachmentGetting()

Each verb object also exposes get, keys, enumerate, taking an AttachmentGetting drawn from a state:

const read = new CommitMutableState(
  CommitStateBuilder.state(db, db.lastCommitId()));
const getting = read.attachmentGetting();

const users = attachments.user_Login.keys(getting);           // Set_Tuto_UserKey
const maybe = attachments.user_Login.get(getting, user);      // Optional_Tuto_Login
if (!maybe.isNil()) {
  console.log(maybe.unwrap().nickname);                        // "alice2"
}

for (const [key, login] of attachments.user_Login.enumerate(getting)) {
  // …
}

The mental model is uniform: a verb is a method on the attachment’s object, taking a getting/mutating handle, a proxy key, and (for setters) a proxy value. Collection-valued attachments add the expected verbs — union<Field>, subtract<Field>, insert<Field>, remove<Field>. The commit itself, and everything about heads, history and merges, is the Commit subsystem’s job, not the SDK’s.

Reading a repro

Because every generated name is mechanical, an error or bug report written in generated symbols maps straight back to your model.

Given a stack trace that mentions attachments.user_Login.set or Tuto_Login, read the name:

  • Tuto_Login → type Login in namespace Tutodata.ts, and the DSM line struct Login { }.

  • attachments.user_Login.set → namespace Tuto, key concept User, attachment login, verb set → the DSM line attachment<User, Login> login;, and the user_Login object in attachments.ts.

From there:

  1. The generated file says “Do not edit by hand.” The defect is upstream — in the DSM model, or in the template that produced the file — never a patch to the output.

  2. Regenerate after any model change (dsm_util.py create_node_package, see dsm_util.py); the package is a build artifact and a stale one will lie about the model.

That legibility is the payoff of the proxy layer: the names you debug are the names you declared.